Direct ByteBuffer performance relative vs absolue de lecture

Alors que je testais les performances de lecture d’un fichier java.nio.ByteBuffer direct, j’ai remarqué que la lecture absolue était en moyenne deux fois plus rapide que la lecture relative. De plus, si je compare le code source de la lecture relative par rapport à la lecture absolue, le code est à peu près le même, à l’exception du fait que la lecture relative conserve et le compteur interne. Je me demande pourquoi je vois une telle différence de vitesse?

Vous trouverez ci-dessous le code source de mon repère 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(); } } 

Et voici les résultats de mon parcours de référence:

 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 

Le test a été exécuté sur un MacbookPro (Intel Core i7 à 2,2 GHz, 16 Gb DDR3) et JDK 1.8.0_73.

METTRE À JOUR

Je lance le même test avec JDK 9-ea b134. Les deux tests montrent une augmentation de vitesse d’environ 10% mais la différence de vitesse entre les deux rest similaire.

 # 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:  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 génère en effet un code pire pour la boucle avec un access relatif à ByteBuffer.

JMH possède perfasm profileur de perfasm qui imprime le code d’assemblage généré pour les régions les plus chaudes. Je l’ai utilisé pour comparer le testReadAbsolute compilé testReadAbsolute testReadRelative , et voici les principales différences:

  1. Relative getLong / getInt/ get champ de position de mise getLong / getInt/ get jour de ByteBuffer . La VM n’optimise pas ces mises à jour: il y a 3 écritures en mémoire sur chaque itération de boucle.

  2. position contrôle de la plage de position n’est pas éliminé: les twigs conditionnelles de chaque itération de boucle sont restées dans le code compilé.

  3. Étant donné que les mises à jour et les vérifications de plage redondantes allongent le corps de la boucle, VM ne déroule que 2 itérations de la boucle. La version compilée de la boucle avec access absolu a 16 itérations déroulées.

testReadAbsolute est très bien compilé: la boucle principale lit seulement 16 longs, les résume et saute à la prochaine itération si index < 10_000_000 - 16 . L'état de directByteBuffer n'est pas mis à jour. Cependant, la machine testReadRelative n'est pas très intelligente pour testReadRelative : semble ne pas pouvoir optimiser l'access au champ d'un object depuis l'extérieur.

JDK 9 avait beaucoup à faire pour optimiser ByteBuffer. J'ai effectué le même test sur JDK 9-ea b134 et vérifié que testReadRelative ne dispose pas d'écritures de mémoire redondantes et de vérifications de plage. Maintenant, il tourne presque aussi vite que 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 

METTRE À JOUR

Afin d'aider le compilateur JIT avec l'optimisation, j'ai introduit la variable locale

 ByteBuffer directByteBuffer = d.directByteBuffer 

dans les deux points de repère. Sinon, le niveau d'indirection ne permet pas au compilateur d'éliminer les mises à jour du champ ByteBuffer.position .