Прямой ByteBuffer относительно абсолютной производительности чтения
Пока я тестировал производительность чтения прямого java.nio.ByteBuffer, я заметил, что абсолютное чтение в среднем в 2 раза быстрее, чем относительное чтение. Кроме того, если я сравниваю исходный код относительного vs абсолютного чтения, код почти такой же, за исключением того, что относительное чтение поддерживает и внутренний счетчик. Интересно, почему я вижу такую значительную разницу в скорости?
Ниже приведен исходный код моего теста 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,2 ГГц Intel Core i7, 16 Гбит DDR3) и JDK 1.8.0_73.
UPDATE
Я запускаю тот же тест с 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
JDK 8 действительно создает худший код для цикла с относительным доступом ByteBuffer.
JMH имеет встроенный профилировщик perfasm
, который печатает сгенерированный код сборки для самых горячих регионов. Я использовал его для сравнения скомпилированных testReadAbsolute
vs. testReadRelative
, и вот основные отличия:
-
Относительно getLong / getInt/ get
обновить поле позиции ByteBuffer
. VM не оптимизирует эти обновления: на каждой итерации цикла записано 3 записи памяти.
-
position
проверка диапазона не устраняется: условные ветки на каждой итерации цикла остались в скомпилированном коде.
-
Поскольку избыточные обновления полей и проверки диапазона делают тело цикла более длинным, VM разворачивает только 2 итерации цикла. Скомпилированная версия для цикла с абсолютным доступом имеет 16 итераций, развернутых.
testReadAbsolute
скомпилирован очень хорошо: основной цикл просто читает 16 длин, суммирует их и перескакивает на следующую итерацию, если 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
UPDATE
Чтобы помочь компилятору JIT с оптимизацией, я ввел локальную переменную
ByteBuffer directByteBuffer = d.directByteBuffer
в обоих тестах. В противном случае уровень косвенности не позволяет компилятору исключить обновления полей ByteBuffer.position
.