Pourquoi est-il long plus lent que l’int en Java x64?

J’utilise Windows 8.1 x64 avec Java 7 update 45 x64 (pas de Java 32 bits installé) sur une tablette Surface Pro 2.

Le code ci-dessous prend 1688 ms lorsque le type de i est long et 109 ms lorsque i est un int. Pourquoi long (un type 64 bits) un ordre de grandeur plus lent que int sur une plate-forme 64 bits avec une JVM 64 bits?

Ma seule spéculation est que le processeur met plus de temps à append un entier de 64 bits à un entier de 32 bits, mais cela semble peu probable. Je soupçonne que Haswell n’utilise pas d’adducteurs de portage.

Je lance ceci dans Eclipse Kepler SR1, btw.

public class Main { private static long i = Integer.MAX_VALUE; public static void main(Ssortingng[] args) { System.out.println("Starting the loop"); long startTime = System.currentTimeMillis(); while(!decrementAndCheck()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the loop in " + (endTime - startTime) + "ms"); } private static boolean decrementAndCheck() { return --i < 0; } } 

Edit: Voici les résultats du code C ++ équivalent compilé par VS 2013 (ci-dessous), même système. long: 72265ms int: 74656ms Ces résultats étaient en mode débogage 32 bits.

En mode de libération 64 bits: long: 875ms long long: 906ms int: 1047ms

Cela suggère que le résultat que j’ai observé est une bizarrerie de l’optimisation de la JVM plutôt que des limitations de la CPU.

 #include "stdafx.h" #include "iostream" #include "windows.h" #include "limits.h" long long i = INT_MAX; using namespace std; boolean decrementAndCheck() { return --i < 0; } int _tmain(int argc, _TCHAR* argv[]) { cout << "Starting the loop" << endl; unsigned long startTime = GetTickCount64(); while (!decrementAndCheck()){ } unsigned long endTime = GetTickCount64(); cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl; } 

Edit: Je viens d’essayer encore ceci dans Java 8 RTM, pas de changement significatif.

Mon JVM fait cette chose assez simple à la boucle interne lorsque vous utilisez s long :

 0x00007fdd859dbb80: test %eax,0x5f7847a(%rip) /* fun JVM hack */ 0x00007fdd859dbb86: dec %r11 /* i-- */ 0x00007fdd859dbb89: mov %r11,0x258(%r10) /* store i to memory */ 0x00007fdd859dbb90: test %r11,%r11 /* unnecessary test */ 0x00007fdd859dbb93: jge 0x00007fdd859dbb80 /* go back to the loop top */ 

Il sortingche, dur, quand vous utilisez int s; D’abord, il y a du vissage que je ne prétends pas comprendre mais qui ressemble à une configuration pour une boucle déroulée:

 0x00007f3dc290b5a1: mov %r11d,%r9d 0x00007f3dc290b5a4: dec %r9d 0x00007f3dc290b5a7: mov %r9d,0x258(%r10) 0x00007f3dc290b5ae: test %r9d,%r9d 0x00007f3dc290b5b1: jl 0x00007f3dc290b662 0x00007f3dc290b5b7: add $0xfffffffffffffffe,%r11d 0x00007f3dc290b5bb: mov %r9d,%ecx 0x00007f3dc290b5be: dec %ecx 0x00007f3dc290b5c0: mov %ecx,0x258(%r10) 0x00007f3dc290b5c7: cmp %r11d,%ecx 0x00007f3dc290b5ca: jle 0x00007f3dc290b5d1 0x00007f3dc290b5cc: mov %ecx,%r9d 0x00007f3dc290b5cf: jmp 0x00007f3dc290b5bb 0x00007f3dc290b5d1: and $0xfffffffffffffffe,%r9d 0x00007f3dc290b5d5: mov %r9d,%r8d 0x00007f3dc290b5d8: neg %r8d 0x00007f3dc290b5db: sar $0x1f,%r8d 0x00007f3dc290b5df: shr $0x1f,%r8d 0x00007f3dc290b5e3: sub %r9d,%r8d 0x00007f3dc290b5e6: sar %r8d 0x00007f3dc290b5e9: neg %r8d 0x00007f3dc290b5ec: and $0xfffffffffffffffe,%r8d 0x00007f3dc290b5f0: shl %r8d 0x00007f3dc290b5f3: mov %r8d,%r11d 0x00007f3dc290b5f6: neg %r11d 0x00007f3dc290b5f9: sar $0x1f,%r11d 0x00007f3dc290b5fd: shr $0x1e,%r11d 0x00007f3dc290b601: sub %r8d,%r11d 0x00007f3dc290b604: sar $0x2,%r11d 0x00007f3dc290b608: neg %r11d 0x00007f3dc290b60b: and $0xfffffffffffffffe,%r11d 0x00007f3dc290b60f: shl $0x2,%r11d 0x00007f3dc290b613: mov %r11d,%r9d 0x00007f3dc290b616: neg %r9d 0x00007f3dc290b619: sar $0x1f,%r9d 0x00007f3dc290b61d: shr $0x1d,%r9d 0x00007f3dc290b621: sub %r11d,%r9d 0x00007f3dc290b624: sar $0x3,%r9d 0x00007f3dc290b628: neg %r9d 0x00007f3dc290b62b: and $0xfffffffffffffffe,%r9d 0x00007f3dc290b62f: shl $0x3,%r9d 0x00007f3dc290b633: mov %ecx,%r11d 0x00007f3dc290b636: sub %r9d,%r11d 0x00007f3dc290b639: cmp %r11d,%ecx 0x00007f3dc290b63c: jle 0x00007f3dc290b64f 0x00007f3dc290b63e: xchg %ax,%ax /* OK, fine; I know what a nop looks like */ 

puis la boucle déroulée elle-même:

 0x00007f3dc290b640: add $0xfffffffffffffff0,%ecx 0x00007f3dc290b643: mov %ecx,0x258(%r10) 0x00007f3dc290b64a: cmp %r11d,%ecx 0x00007f3dc290b64d: jg 0x00007f3dc290b640 

puis le code de déassembly de la boucle déroulée, lui-même un test et une boucle droite:

 0x00007f3dc290b64f: cmp $0xffffffffffffffff,%ecx 0x00007f3dc290b652: jle 0x00007f3dc290b662 0x00007f3dc290b654: dec %ecx 0x00007f3dc290b656: mov %ecx,0x258(%r10) 0x00007f3dc290b65d: cmp $0xffffffffffffffff,%ecx 0x00007f3dc290b660: jg 0x00007f3dc290b654 

Donc, cela va 16 fois plus vite pour les ints car le JIT a déroulé la boucle int 16 fois, mais n’a pas déroulé la long boucle du tout.

Pour être complet, voici le code que j’ai réellement essayé:

 public class foo136 { private static int i = Integer.MAX_VALUE; public static void main(Ssortingng[] args) { System.out.println("Starting the loop"); for (int foo = 0; foo < 100; foo++) doit(); } static void doit() { i = Integer.MAX_VALUE; long startTime = System.currentTimeMillis(); while(!decrementAndCheck()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the loop in " + (endTime - startTime) + "ms"); } private static boolean decrementAndCheck() { return --i < 0; } } 

Les -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly assemblage ont été générés à l'aide des options -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly . Notez que vous devez modifier l'installation de votre JVM pour que cela fonctionne également pour vous. vous devez placer une bibliothèque partagée aléatoire exactement au bon endroit ou elle échouera.

La stack JVM est définie en termes de mots , dont la taille est un détail d’implémentation mais doit avoir au moins 32 bits de large. L’implémenteur JVM peut utiliser des mots de 64 bits, mais le bytecode ne peut pas s’y fier, de sorte que les opérations avec long valeurs long ou double doivent être traitées avec un soin particulier. En particulier, les instructions de twig entière JVM sont définies exactement sur le type int .

Dans le cas de votre code, le déassembly est instructif. Voici le bytecode de la version int compilée par Oracle JDK 7:

 private static boolean decrementAndCheck(); Code: 0: getstatic #14 // Field i:I 3: iconst_1 4: isub 5: dup 6: putstatic #14 // Field i:I 9: ifge 16 12: iconst_1 13: goto 17 16: iconst_0 17: ireturn 

Notez que la JVM va charger la valeur de votre statique i (0), soustraire un (3-4), dupliquer la valeur sur la stack (5) et la replacer dans la variable (6). Il fait ensuite une twig de comparaison avec zéro et retourne.

La version avec le long est un peu plus compliquée:

 private static boolean decrementAndCheck(); Code: 0: getstatic #14 // Field i:J 3: lconst_1 4: lsub 5: dup2 6: putstatic #14 // Field i:J 9: lconst_0 10: lcmp 11: ifge 18 14: iconst_1 15: goto 19 18: iconst_0 19: ireturn 

Premièrement, lorsque la machine virtuelle Java duplique la nouvelle valeur sur la stack (5), elle doit dupliquer deux mots de stack. Dans votre cas, il est tout à fait possible que cela ne soit pas plus coûteux que d’en dupliquer une, car la JVM est libre d’utiliser un mot de 64 bits si cela est pratique. Cependant, vous remarquerez que la logique de twig est plus longue ici. La JVM n’a pas d’instructions pour comparer un long avec zéro, elle doit donc pousser une constante 0L sur la stack (9), faire une comparaison long générale (10), puis se reporter à la valeur de ce calcul.

Voici deux scénarios plausibles:

  • La JVM suit exactement le chemin du bytecode. Dans ce cas, cela fait plus de travail dans la version long , poussant et écrasant plusieurs valeurs supplémentaires, celles-ci se trouvant sur la stack gérée virtuelle , et non sur la vraie stack de CPU assistée par matériel. Si tel est le cas, vous verrez toujours une différence de performance significative après le réchauffement.
  • La JVM se rend compte qu’elle peut optimiser ce code. Dans ce cas, il faut plus de temps pour optimiser une partie de la logique push / compare pratiquement inutile. Si tel est le cas, vous verrez très peu de différence de performance après le réchauffement.

Je vous recommande d’ écrire un microbenchmark correct pour éliminer l’effet de lancer le JIT, et aussi d’essayer avec une condition finale qui n’est pas nulle, de forcer la JVM à faire la même comparaison sur le même .

L’unité de base des données dans une machine virtuelle Java est word. Le choix de la bonne taille de mot est laissé lors de la mise en œuvre de la JVM. Une implémentation JVM doit choisir une taille de mot minimale de 32 bits. Il peut choisir une taille de mot plus élevée pour gagner en efficacité. Il n’y a pas non plus de ressortingction à ce qu’une JVM 64 bits choisisse un mot de 64 bits uniquement.

L’architecture sous-jacente ne stipule pas que la taille du mot doit également être la même. JVM lit / écrit des données mot par mot. C’est la raison pour laquelle cela pourrait prendre plus longtemps pour un long temps que pour un int .

Ici vous pouvez trouver plus sur le même sujet.

Je viens d’écrire un benchmark en utilisant caliper .

Les résultats sont assez cohérents avec le code d’origine: une accélération de ~ 12x pour utiliser int sur une long . Il semble bien que le déroulement de la boucle signalé par tmyklebu ou quelque chose de très similaire se produise .

 timeIntDecrements 195,266,845.000 timeLongDecrements 2,321,447,978.000 

Ceci est mon code Notez qu’il utilise un instantané de caliper fraîchement construit, car je ne pouvais pas comprendre comment coder contre leur version bêta existante.

 package test; import com.google.caliper.Benchmark; import com.google.caliper.Param; public final class App { @Param({""+1}) int number; private static class IntTest { public static int v; public static void reset() { v = Integer.MAX_VALUE; } public static boolean decrementAndCheck() { return --v < 0; } } private static class LongTest { public static long v; public static void reset() { v = Integer.MAX_VALUE; } public static boolean decrementAndCheck() { return --v < 0; } } @Benchmark int timeLongDecrements(int reps) { int k=0; for (int i=0; i 

Pour mémoire, cette version fait un “warmup” brut:

 public class LongSpeed { private static long i = Integer.MAX_VALUE; private static int j = Integer.MAX_VALUE; public static void main(Ssortingng[] args) { for (int x = 0; x < 10; x++) { runLong(); runWord(); } } private static void runLong() { System.out.println("Starting the long loop"); i = Integer.MAX_VALUE; long startTime = System.currentTimeMillis(); while(!decrementAndCheckI()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the long loop in " + (endTime - startTime) + "ms"); } private static void runWord() { System.out.println("Starting the word loop"); j = Integer.MAX_VALUE; long startTime = System.currentTimeMillis(); while(!decrementAndCheckJ()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the word loop in " + (endTime - startTime) + "ms"); } private static boolean decrementAndCheckI() { return --i < 0; } private static boolean decrementAndCheckJ() { return --j < 0; } } 

Les temps globaux s'améliorent d'environ 30%, mais le rapport entre les deux rest à peu près le même.

Pour les records:

si j’utilise

 boolean decrementAndCheckLong() { lo = lo - 1l; return lo < -1l; } 

(changé "l--" à "l = l - 1l") la performance longue améliore de ~ 50%

Je n’ai pas de machine 64 bits à tester, mais la différence assez grande suggère qu’il y a plus que le bytecode légèrement plus long au travail.

Je vois des temps très courts pour long / int (4400 vs 4800ms) sur mon 1.7.0_45 32 bits.

Ce n’est qu’une supposition , mais je soupçonne fortement que c’est l’effet d’une pénalité de désalignement de la mémoire. Pour confirmer / rejeter la suspicion, essayez d’append un dummy int statique public = 0; avant la déclaration de i. Cela réduira de 4 octets la disposition de la mémoire et pourrait bien l’aligner pour de meilleures performances. Confirmé de ne pas causer le problème.

MODIFIER: La raison en est que la machine virtuelle peut ne pas réordonner les champs à loisir en ajoutant un remplissage pour un alignement optimal, car cela peut interférer avec JNI. (Pas le cas).