直接 ByteBuffer 相对与绝对读取性能
Direct ByteBuffer relative vs absolute read performance
当我测试直接 java.nio.ByteBuffer 的读取性能时,我注意到绝对读取平均比相对读取快 2 倍。此外,如果我比较相对读取与绝对读取的源代码,除了相对读取维护和内部计数器外,代码几乎相同。我想知道为什么我看到速度有这么大的差异?
下面是我的 JMH 基准测试的源代码:
public class DirectByteBufferReadBenchmark {
private static final int OBJ_SIZE = 8 + 4 + 1;
private static final int NUM_ELEM = 10_000_000;
@State(Scope.Benchmark)
public static class Data {
private ByteBuffer directByteBuffer;
@Setup
public void setup() {
directByteBuffer = ByteBuffer.allocateDirect(OBJ_SIZE * NUM_ELEM);
for (int i = 0; i < NUM_ELEM; i++) {
directByteBuffer.putLong(i);
directByteBuffer.putInt(i);
directByteBuffer.put((byte) (i & 1));
}
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public long testReadAbsolute(Data d) throws InterruptedException {
long val = 0l;
for (int i = 0; i < NUM_ELEM; i++) {
int index = OBJ_SIZE * i;
val += d.directByteBuffer.getLong(index);
d.directByteBuffer.getInt(index + 8);
d.directByteBuffer.get(index + 12);
}
return val;
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public long testReadRelative(Data d) throws InterruptedException {
d.directByteBuffer.rewind();
long val = 0l;
for (int i = 0; i < NUM_ELEM; i++) {
val += d.directByteBuffer.getLong();
d.directByteBuffer.getInt();
d.directByteBuffer.get();
}
return val;
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(DirectByteBufferReadBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(3)
.threads(1)
.build();
new Runner(opt).run();
}
}
这些是我的基准测试结果 运行:
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 15 88.605 ± 9.276 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 15 42.904 ± 3.018 ops/s
测试是在 MacbookPro(2.2GHz Intel Core i7、16Gb DDR3)和 JDK 1.8.0_73.
上 运行
更新
我运行 与JDK 9-ea b134 进行了相同的测试。两项测试均显示速度提高了约 10%,但两者之间的速度差异仍然相似。
# JMH 1.13 (released 45 days ago)
# VM version: JDK 9-ea, VM 9-ea+134
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/bin/java
# VM options: <none>
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 15 102.170 ± 10.199 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 15 45.988 ± 3.896 ops/s
JDK 8 确实会为具有相对 ByteBuffer 访问权限的循环生成更糟糕的代码。
JMH 具有内置的 perfasm
分析器,可以为最热的区域打印生成的汇编代码。我已经 used it to compare 编译了 testReadAbsolute
与 testReadRelative
,主要区别如下:
相对 getLong / getInt/ get
更新 ByteBuffer
的位置字段。 VM 不优化这些更新:每个循环迭代有 3 次内存写入。
position
范围检查未消除:每个循环迭代的条件分支保留在编译代码中。
由于冗余字段更新和范围检查使循环体变长,VM 仅展开循环的 2 次迭代。具有绝对访问权限的循环的编译版本展开了 16 次迭代。
testReadAbsolute
编译得很好:主循环只读取 16 个 long,将它们相加,如果 index < 10_000_000 - 16
则跳转到下一个迭代。 directByteBuffer
的状态未更新。然而,JVM 对于 testReadRelative
来说并不是那么聪明:似乎它无法优化从外部访问对象的字段。
JDK9 中有很多工作来优化 ByteBuffer。我在 JDK 9-ea b134 上进行了 运行 相同的测试,并验证了 testReadRelative
没有冗余内存写入和范围检查。现在 运行 几乎和 testReadAbsolute
一样快。
// JDK 1.8.0_92, VM 25.92-b14
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 10 99,727 ± 0,542 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 10 47,126 ± 0,289 ops/s
// JDK 9-ea, VM 9-ea+134
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 10 109,369 ± 0,403 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 10 97,140 ± 0,572 ops/s
更新
为了帮助 JIT 编译器进行优化,我引入了局部变量
ByteBuffer directByteBuffer = d.directByteBuffer
在两个基准测试中。否则间接级别不允许编译器消除 ByteBuffer.position
字段更新。
当我测试直接 java.nio.ByteBuffer 的读取性能时,我注意到绝对读取平均比相对读取快 2 倍。此外,如果我比较相对读取与绝对读取的源代码,除了相对读取维护和内部计数器外,代码几乎相同。我想知道为什么我看到速度有这么大的差异?
下面是我的 JMH 基准测试的源代码:
public class DirectByteBufferReadBenchmark {
private static final int OBJ_SIZE = 8 + 4 + 1;
private static final int NUM_ELEM = 10_000_000;
@State(Scope.Benchmark)
public static class Data {
private ByteBuffer directByteBuffer;
@Setup
public void setup() {
directByteBuffer = ByteBuffer.allocateDirect(OBJ_SIZE * NUM_ELEM);
for (int i = 0; i < NUM_ELEM; i++) {
directByteBuffer.putLong(i);
directByteBuffer.putInt(i);
directByteBuffer.put((byte) (i & 1));
}
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public long testReadAbsolute(Data d) throws InterruptedException {
long val = 0l;
for (int i = 0; i < NUM_ELEM; i++) {
int index = OBJ_SIZE * i;
val += d.directByteBuffer.getLong(index);
d.directByteBuffer.getInt(index + 8);
d.directByteBuffer.get(index + 12);
}
return val;
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public long testReadRelative(Data d) throws InterruptedException {
d.directByteBuffer.rewind();
long val = 0l;
for (int i = 0; i < NUM_ELEM; i++) {
val += d.directByteBuffer.getLong();
d.directByteBuffer.getInt();
d.directByteBuffer.get();
}
return val;
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(DirectByteBufferReadBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(3)
.threads(1)
.build();
new Runner(opt).run();
}
}
这些是我的基准测试结果 运行:
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 15 88.605 ± 9.276 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 15 42.904 ± 3.018 ops/s
测试是在 MacbookPro(2.2GHz Intel Core i7、16Gb DDR3)和 JDK 1.8.0_73.
上 运行更新
我运行 与JDK 9-ea b134 进行了相同的测试。两项测试均显示速度提高了约 10%,但两者之间的速度差异仍然相似。
# JMH 1.13 (released 45 days ago)
# VM version: JDK 9-ea, VM 9-ea+134
# VM invoker: /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/bin/java
# VM options: <none>
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 15 102.170 ± 10.199 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 15 45.988 ± 3.896 ops/s
JDK 8 确实会为具有相对 ByteBuffer 访问权限的循环生成更糟糕的代码。
JMH 具有内置的 perfasm
分析器,可以为最热的区域打印生成的汇编代码。我已经 used it to compare 编译了 testReadAbsolute
与 testReadRelative
,主要区别如下:
相对
getLong / getInt/ get
更新ByteBuffer
的位置字段。 VM 不优化这些更新:每个循环迭代有 3 次内存写入。position
范围检查未消除:每个循环迭代的条件分支保留在编译代码中。由于冗余字段更新和范围检查使循环体变长,VM 仅展开循环的 2 次迭代。具有绝对访问权限的循环的编译版本展开了 16 次迭代。
testReadAbsolute
编译得很好:主循环只读取 16 个 long,将它们相加,如果 index < 10_000_000 - 16
则跳转到下一个迭代。 directByteBuffer
的状态未更新。然而,JVM 对于 testReadRelative
来说并不是那么聪明:似乎它无法优化从外部访问对象的字段。
JDK9 中有很多工作来优化 ByteBuffer。我在 JDK 9-ea b134 上进行了 运行 相同的测试,并验证了 testReadRelative
没有冗余内存写入和范围检查。现在 运行 几乎和 testReadAbsolute
一样快。
// JDK 1.8.0_92, VM 25.92-b14
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 10 99,727 ± 0,542 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 10 47,126 ± 0,289 ops/s
// JDK 9-ea, VM 9-ea+134
Benchmark Mode Cnt Score Error Units
DirectByteBufferReadBenchmark.testReadAbsolute thrpt 10 109,369 ± 0,403 ops/s
DirectByteBufferReadBenchmark.testReadRelative thrpt 10 97,140 ± 0,572 ops/s
更新
为了帮助 JIT 编译器进行优化,我引入了局部变量
ByteBuffer directByteBuffer = d.directByteBuffer
在两个基准测试中。否则间接级别不允许编译器消除 ByteBuffer.position
字段更新。