Stu*_*son 31 java nio bytebuffer
我工作的一些SocketChannel至- SocketChannel代码会做最好用直接字节缓冲区- (几十到几百每个连接的兆字节),长寿命,大而散列出具有确切循环结构FileChannelS,我跑了一些微在基准测试ByteBuffer.allocate()与ByteBuffer.allocateDirect()性能.
结果出人意料,我无法解释.在下图中,对于ByteBuffer.allocate()传输实现,在256KB和512KB处有一个非常明显的悬崖- 性能下降了~50%!这似乎也是一个较小的性能悬崖ByteBuffer.allocateDirect().(%-gain系列有助于可视化这些变化.)
缓冲区大小(字节)与时间(MS)

为什么奇数性能曲线ByteBuffer.allocate()与ByteBuffer.allocateDirect()?之间存在差异? 幕后究竟发生了什么?
它很可能取决于硬件和操作系统,所以这里有以下细节:
源代码,按要求:
package ch.dietpizza.bench;
import static java.lang.String.format;
import static java.lang.System.out;
import static java.nio.ByteBuffer.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class SocketChannelByteBufferExample {
private static WritableByteChannel target;
private static ReadableByteChannel source;
private static ByteBuffer buffer;
public static void main(String[] args) throws IOException, InterruptedException {
long timeDirect;
long normal;
out.println("start");
for (int i = 512; i <= 1024 * 1024 * 64; i *= 2) {
buffer = allocateDirect(i);
timeDirect = copyShortest();
buffer = allocate(i);
normal = copyShortest();
out.println(format("%d, %d, %d", i, normal, timeDirect));
}
out.println("stop");
}
private static long copyShortest() throws IOException, InterruptedException {
int result = 0;
for (int i = 0; i < 100; i++) {
int single = copyOnce();
result = (i == 0) ? single : Math.min(result, single);
}
return result;
}
private static int copyOnce() throws IOException, InterruptedException {
initialize();
long start = System.currentTimeMillis();
while (source.read(buffer)!= -1) {
buffer.flip();
target.write(buffer);
buffer.clear(); //pos = 0, limit = capacity
}
long time = System.currentTimeMillis() - start;
rest();
return (int)time;
}
private static void initialize() throws UnknownHostException, IOException {
InputStream is = new FileInputStream(new File("/Users/stu/temp/robyn.in"));//315 MB file
OutputStream os = new FileOutputStream(new File("/dev/null"));
target = Channels.newChannel(os);
source = Channels.newChannel(is);
}
private static void rest() throws InterruptedException {
System.gc();
Thread.sleep(200);
}
}
Run Code Online (Sandbox Code Playgroud)
bes*_*sss 27
首先,我有点惊讶,这不是常识,但承担它与我
直接字节缓冲区在java堆外部分配一个地址.
这是至关重要的:所有OS(和本机C)函数都可以利用该地址来锁定堆上的对象并复制数据.复制的简短示例:为了通过Socket.getOutputStream().write(byte [])发送任何数据,本机代码必须"锁定"byte [],将其复制到java堆外部,然后调用OS函数,例如发送.复制在堆栈上执行(对于较小的字节[])或通过malloc/free对较大的执行.DatagramSockets没有什么不同,它们也可以复制 - 除了它们被限制为64KB并在堆栈上分配,如果线程堆栈不够大或递归深度,它甚至可以终止进程. 注意:锁定可防止JVM/GC在堆周围移动/重新分配对象
因此,在引入NIO时,我们的想法是避免复制流和大量流管道/间接.在数据到达目的地之前,通常有3-4种缓冲类型的流.(yay波兰用漂亮的镜头均衡(!))
通过引入直接缓冲区,java可以直接与C本机代码进行通信,而无需任何锁定/复制.因此,sent函数可以取缓冲区的地址添加位置,并且性能与本机C的性能大致相同.这是关于直接缓冲区的.
带有直接缓冲区的主要问题 - 它们分配起来很昂贵,而且解除分配成本高昂且使用起来非常繁琐,就像byte []一样.
非直接缓冲区不提供直接缓冲区所做的真正本质 - 即直接桥接到本机/操作系统而不是轻量级并且共享完全相同的API - 甚至更多,它们wrap byte[]甚至可以使用它们的后备阵列直接操纵 - 什么不爱?好吧,他们必须被复制!
那么Sun/Oracle如何处理非直接缓冲区,因为操作系统/本机不能使用'em - 好吧,天真.当使用非直接缓冲区时,必须创建直接计数器部分.该实现足够智能,可以ThreadLocal通过SoftReference*使用和缓存一些直接缓冲区,以避免创建的高成本.复制它们时会出现天真的部分 - 它remaining()每次都会尝试复制整个缓冲区().
现在想象一下:512 KB非直接缓冲区转到64 KB套接字缓冲区,套接字缓冲区不会超过它的大小.所以第一次512 KB将从非直接复制到线程本地直接,但只使用64 KB.下一次将复制512-64 KB但仅使用64 KB,第三次将复制512-64*2 KB,但将仅使用64 KB,依此类推...而且乐观地认为套接字总是如此缓冲区将完全为空.因此,您不仅要复制nKB,而且要求n× n÷ m(n= 512,m= 16(套接字缓冲区的平均空间)).
复制部分是所有非直接缓冲区的公共/抽象路径,因此实现永远不会知道目标容量.复制会破坏缓存,什么不会,减少内存带宽等.
*关于SoftReference缓存的说明:它取决于GC的实现,经验可能会有所不同.Sun的GC使用空闲堆内存来确定SoftRefences的生命周期,当它们被释放时会导致一些尴尬的行为 - 应用程序需要再次分配先前缓存的对象 - 即更多分配(直接ByteBuffers占用堆中的一小部分,所以至少他们不会影响额外的缓存垃圾,但会受到影响)
我的拇指规则 - 一个汇集的直接缓冲区,使用套接字读/写缓冲区调整大小.操作系统永远不会复制超过必要的.
这个微基准测试主要是内存吞吐量测试,操作系统将文件完全放在缓存中,所以它主要是测试memcpy.一旦缓冲区用完L2缓存,性能下降就会明显.同样运行基准也会增加和累积GC收集成本.(rest()不会收集软引用的ByteBuffers)
Ber*_*t F 26
我想知道测试期间线程局部分配缓冲区(TLAB)是否在256K左右.使用TLAB优化堆的分配,以便<= 256K的非直接分配很快.
通常做的是为每个线程提供一个缓冲区,该缓冲区由该线程专门用于进行分配.您必须使用一些同步来从堆中分配缓冲区,但之后线程可以从缓冲区分配而不进行同步.在热点JVM中,我们将这些称为线程本地分配缓冲区(TLAB).他们运作良好.
如果我关于256K TLAB的假设是正确的,那么本文后面的信息表明,大型非直接缓冲区的> 256K分配可能会绕过TLAB.这些分配直接进入堆,需要线程同步,从而导致性能命中.
无法从TLAB进行的分配并不总是意味着线程必须获得新的TLAB.根据分配的大小和TLAB中剩余的未使用空间,VM可以决定只从堆中进行分配.来自堆的分配需要同步,但是获得新的TLAB也是如此.如果分配被认为是大的(当前TLAB大小的一小部分),则分配将始终在堆外完成.这减少了浪费,优雅地处理了大于平均水平的分配.
可以使用后来文章中的信息来测试此假设,该文章指示如何调整TLAB并获取诊断信息:
要试验特定的TLAB大小,需要设置两个-XX标志,一个用于定义初始大小,另一个用于禁用大小调整:
Run Code Online (Sandbox Code Playgroud)-XX:TLABSize= -XX:-ResizeTLABtlab的最小大小使用-XX:MinTLABSize设置,默认为2K字节.最大大小是整数Java数组的最大大小,用于在发生GC清除时填充TLAB的未分配部分.
诊断打印选项
Run Code Online (Sandbox Code Playgroud)-XX:+PrintTLAB每个线程打印每行扫描一行(以"TLAB:gc thread:"开头,不带"s")和一个摘要行.
我怀疑这些膝盖是由于跨越CPU缓存边界而跳闸.与"直接"缓冲区read()/ write()实现相比,由于额外的内存缓冲区复制,"非直接"缓冲区read()/ write()实现"缓存未命中".
发生这种情况的原因有很多。如果没有代码和/或有关数据的更多详细信息,我们只能猜测发生了什么。
一些猜测:
-Xmx和-Xms参数| 归档时间: |
|
| 查看次数: |
10795 次 |
| 最近记录: |