byt*_*101 5 java performance jvm bytebuffer jmh
我正在尝试优化一个简单的解压缩例程,并遇到了这个奇怪的性能怪癖,我似乎找不到太多信息:手动实现的简单字节缓冲区比内置字节缓冲区(堆)快 10%-20% & 映射)用于简单操作(读取一个字节,读取 n 个字节,是否是流末尾)
\n我测试了3个API:
\nByteBuffer.wrap(byte[])简单的包装器:
\nclass TestBuf {\n private final byte[] ary;\n private int pos = 0;\n\n public TestBuf(ByteBuffer buffer) { // ctor #1\n ary = new byte[buffer.remaining()];\n buffer.get(ary);\n }\n \n public TestBuf(byte[] inAry) { // ctor #2\n ary = inAry;\n }\n\n public int readUByte() { return ary[pos++] & 0xFF; }\n\n public boolean hasRemaining() { return pos < ary.length; }\n\n public void get(byte[] out, int offset, int length) {\n System.arraycopy(ary, pos, out, offset, length);\n pos += length;\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n我的主循环的精简核心大致是以下模式:
\nwhile (buffer.hasRemaining()) {\n int op = buffer.readUByte();\n if (op == 1) {\n int size = buffer.readUByte();\n buffer.get(outputArray, outputPos, size);\n outputPos += size;\n } // ...\n}\nRun Code Online (Sandbox Code Playgroud)\n我测试了以下组合:
\nnative-array:传递byte[]给byte[]-accepting 方法(无副本)native-testbuf:传递byte[]给将其包装在 a 中的方法TestBuf(无副本,ctor #2)native-buffer:传递ByteBuffer.wrap(byte[])给 ByteBuffer 接受方法(无副本)buffer-array:传递ByteBuffer.wrap(byte[])给将 ByteBuffer 提取到数组的方法buffer-testbuf:传递ByteBuffer.wrap(byte[])给将 ByteBuffer 提取到TestBuf(ctor #1)中的数组的方法我使用 JMH(黑洞化每个输出数组),并在 OpenJDK 和 GraalVM 上测试了 Java 17,并使用预加载到 RAM 中的约 5GiB 的解压语料库,其中包含约 150,000 个项目,平均大小从 2KiB 到 15MiB。每个语料库需要约 10 秒的时间来解压,并且 JMH 运行进行了适当的预热和迭代。我确实将测试剥离了最少必要的非数组代码,但即使对原始代码进行基准测试,差异也几乎相同的百分比(即,我认为除了缓冲区/数组之外没有太多其他内容)访问控制我的原始代码的性能)
\n在多台计算机上,结果有点不稳定,但相对一致:
\nnative-array和native-testbuf最快的选项,由于优化器的帮助,误差范围内(低于 0.5%)(9.3 秒/语料库)native-buffer始终是最慢的选择。这总是比最快的慢 17-22% native-array/native-testbuf (11.4s/语料库)buffer-array和buffer-testbuf处于中间位置,彼此之间的差距约为 1%,但比 慢约 4-7% native-array。然而,尽管它们产生了额外的数组副本,但它们总是比native-buffer约 15-17%。(9.7 秒/语料库)其中两个结果最令我惊讶:
\nnative-buffer非常慢(native-testbuf通过buffer-*) 仍然比使用ByteBuffer.wrap对象 (native-buffer )我尝试四处查找有关我可能做错的信息,但据我所知,大多数性能问题都与本机内存和有关,而我MappedByteBuffers正在使用。与我重新实现的简单读取访问相比,HeapByteBuffers为什么这么慢?HeapByteBuffers有什么方法可以HeapByteBuffers更有效地使用吗?这是否也适用于MappedByteBuffer?
更新:我已经在https://gist.github.com/byteit101/84a3ab8f292de404e122562c7008c133上发布了完整的基准测试、语料库生成器和算法请注意,在尝试让语料库生成器工作时,我发现我的 24 位数字导致性能损失,因此添加了一个buffer-bufer目标,其中将缓冲区复制到新缓冲区并使用新缓冲区比在 24 位数字之后使用原始缓冲区更快。
使用生成的语料库在我的一台机器上运行:
\nBenchmark Mode Cnt Score Error Units\nSOBench.t1_native_array ss 60 0.891 \xc2\xb1 0.018 s/op\nSOBench.t2_buffer_testbuf ss 60 0.899 \xc2\xb1 0.024 s/op\nSOBench.t3_buffer_buffer ss 60 0.935 \xc2\xb1 0.024 s/op\nSOBench.t4_native_buffer ss 60 1.099 \xc2\xb1 0.024 s/op\nRun Code Online (Sandbox Code Playgroud)\n最近的一些观察结果:删除未使用的代码(请参阅要点中的注释)使 ByteBuffer 与本机数组一样快,轻微的调整(更改逻辑比较的位掩码条件)也是如此,所以我当前的理论是,它是一些内联缓存未命中也与偏移量相关
\n小智 2
我认为 Java 17 存在回归。我正在使用一个处理字符串的库。通过#String.Split 或#String.getBytes 多次创建新副本。所以我尝试了使用 ByteBuffer 的替代实现。
在 Java 11 中,该解决方案比原始基于字符串的版本快了约 30%。
time: 129 vs 180 ns/op
gc.alloc.rate: 2931 vs 3861 MB/sec
gc.count: 300 vs 323
gc.time: 172 vs 178 ms
Run Code Online (Sandbox Code Playgroud)
在 Java 17 中,情况发生了变化。ByteBuffer 版本恶化,String 版本改进。
time: 143 vs 146 ns/op
gc.alloc.rate: 2889 vs 4781 MB/sec
gc.count: 426 vs 586
gc.time: 240 vs 305 ms
Run Code Online (Sandbox Code Playgroud)
甚至 gc.count 和 gc.time 也增加了。