直接字节缓冲区的相对读取性能与绝对读取性能比较

14

在测试直接使用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
1个回答

19

JDK 8相对于通过ByteBuffer访问的循环确实生成了更劣的代码。

JMH内置perfasm分析器,可以打印最热点区域生成的汇编代码。我使用它进行比较编译后的testReadAbsolutetestReadRelative,以下是主要差异:

  1. 相对位置的getLong/getInt/get更新ByteBuffer的position字段。虚拟机不会优化这些更新:每次循环迭代有3个内存写入。

  2. position范围检查未被消除:每次循环迭代中都存在条件分支。

  3. 由于冗余的字段更新和范围检查使循环体变得更长,因此VM仅展开2次循环迭代。具有绝对访问的循环的编译版本展开了16次迭代。

testReadAbsolute编译得非常好:主循环只读取16个long,将它们相加并跳到下一个迭代(如果index < 10_000_000 - 16)。directByteBuffer的状态没有更新。然而,JVM对于testReadRelative并不那么智能:似乎它无法优化对象外部的字段访问。

JDK 9进行了大量优化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 字段更新。


谢谢您的回答。我已经使用JDK 9进行了测试,可以在问题更新中看到,但是我没有看到相对读取性能有很大提升。有什么想法吗? - Vladimir G.
@VladimirG。是的,我的基准测试确实有点不同。我已经更新了答案。原因仍然相同:JIT不会优化掉position字段的更新,这就是为什么相对ByteBuffer访问似乎不太高效的原因。 - apangin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接