Pourquoi Java active-t-il les entrées contiguës semble-t-il s'exécuter plus rapidement avec des cas supplémentaires?

276

Je travaille sur du code Java qui doit être hautement optimisé car il fonctionnera dans des fonctions chaudes qui sont invoquées à de nombreux points dans ma logique de programme principale. Une partie de ce code implique la multiplication des doublevariables par des 10valeurs élevées à des valeurs arbitraires non négatives int exponent. Une façon rapide (edit: mais pas le plus rapide possible, voir Mise à jour 2 ci - dessous) pour obtenir la valeur multipliée est switchle exponent:

double multiplyByPowerOfTen(final double d, final int exponent) {
   switch (exponent) {
      case 0:
         return d;
      case 1:
         return d*10;
      case 2:
         return d*100;
      // ... same pattern
      case 9:
         return d*1000000000;
      case 10:
         return d*10000000000L;
      // ... same pattern with long literals
      case 18:
         return d*1000000000000000000L;
      default:
         throw new ParseException("Unhandled power of ten " + power, 0);
   }
}

Les ellipses commentées ci-dessus indiquent que les case intconstantes continuent à augmenter de 1, il y a donc vraiment 19 cases dans l'extrait de code ci-dessus. Puisque je ne suis pas sûr que je réellement besoin de tous les pouvoirs de 10 casedéclarations à 10travers 18, j'ai couru quelques microbenchmarks comparant le temps aux opérations complètes de 10 millions cette switchdéclaration par rapport à un switchavec seulement cases à 0travers 9(avec le exponentlimité à 9 ou moins éviter de casser le pared-down switch). J'ai obtenu le résultat plutôt surprenant (du moins pour moi!) Que le plus long switchavec plus d' caseinstructions s'exécutait plus vite.

Sur une alouette, j'ai essayé d'ajouter encore plus de cases qui viennent de renvoyer des valeurs fictives, et j'ai constaté que je pouvais faire fonctionner le commutateur encore plus rapidement avec environ 22-27 cases déclarés (même si ces cas fictifs ne sont jamais réellement frappés pendant que le code est en cours d'exécution) ). (Encore une fois, les cases ont été ajoutés de manière contiguë en incrémentant la caseconstante précédente de 1.) Ces différences de temps d'exécution ne sont pas très importantes: pour une aléatoire exponententre 0et10 , l' switchinstruction factice remplie termine 10 millions d'exécutions en 1,49 seconde contre 1,54 seconde pour la non remplie version, pour une économie totale de 5 ns par exécution. Donc, pas le genre de chose qui rend obsédant le rembourrage d'unswitchdéclaration vaut l'effort d'un point de vue d'optimisation. Mais je trouve toujours curieux et contre-intuitif qu'un a switchne devienne pas plus lent (ou peut-être au mieux maintienne un temps O (1) constant ) à exécuter car plus de cases y sont ajoutés.

changer les résultats d'analyse comparative

Ce sont les résultats que j'ai obtenus en exécutant avec différentes limites sur les exponentvaleurs générées aléatoirement . Je n'ai pas inclus les résultats jusqu'à 1la exponentlimite, mais la forme générale de la courbe reste la même, avec une crête autour de la marque de cas 12-17 et une vallée entre 18-28. Tous les tests ont été exécutés dans JUnitBenchmarks à l'aide de conteneurs partagés pour les valeurs aléatoires afin de garantir des entrées de test identiques. J'ai également exécuté les tests à la fois du plus long switchau plus court et vice-versa, pour essayer d'éliminer la possibilité de problèmes de test liés à la commande. J'ai mis mon code de test sur un dépôt github si quelqu'un veut essayer de reproduire ces résultats.

Alors, que se passe-t-il ici? Des aléas de mon architecture ou de ma construction de micro-benchmark? Ou est-ce que Java est switchvraiment un peu plus rapide à exécuter dans la plage de 18destination 28 caseque 11jusqu'à 17?

github test repo "switch-experiment"

MISE À JOUR: J'ai nettoyé un peu la bibliothèque de benchmarking et ajouté un fichier texte dans / results avec une sortie sur une plus large gamme de exponentvaleurs possibles . J'ai également ajouté une option dans le code de test pour ne pas lancer Exceptionde default, mais cela ne semble pas affecter les résultats.

MISE À JOUR 2: Trouvé une discussion assez bonne sur ce problème depuis 2009 sur le forum xkcd ici: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . La discussion de l'OP sur l'utilisation Array.binarySearch()m'a donné l'idée d'une implémentation simple basée sur un tableau du modèle d'exponentiation ci-dessus. Il n'y a pas besoin de la recherche binaire car je sais quelles sont les entrées dans le array. Il semble fonctionner environ 3 fois plus vite que son utilisation switch, évidemment au détriment d'une partie du flux de contrôle switchfourni. Ce code a également été ajouté au dépôt github.

Andrew Bissell
la source
64
Maintenant, tous les Googlers du monde entier auront précisément 22 cas dans toutes les switchdéclarations, car c'est clairement la solution la plus optimale. : D (Ne montrez pas cela à mon avance, s'il vous plaît.)
asteri
2
Avez-vous un SSCCE plus simple? Celui-ci ne compile pas pour moi. Aussi faible que je sois avec les performances Java, je veux essayer.
Mysticial
5
Vous trouverez peut-être utile la section "Commutateurs dans la JVM" dans ma réponse sur les cas basés sur des chaînes. Je pense que ce qui se passe ici, c'est que vous passez de a lookupswitchà a tableswitch. Le démontage de votre code avec javapvous montrerait à coup sûr.
erickson
2
J'ai ajouté les pots de dépendance au dossier / lib dans le dépôt. @Mysticial Désolé, j'ai déjà passé trop de temps à descendre ce terrier de lapin! Si vous supprimez les "étendues AbstractBenchmark" des classes de test et que vous vous débarrassez des importations "com.carrotsearch", vous pouvez exécuter uniquement la dépendance JUnit, mais la fonction de recherche de carottes est assez pratique pour filtrer une partie du bruit du JIT et les périodes d'échauffement. Malheureusement, je ne sais pas comment exécuter ces tests JUnit en dehors d'IntelliJ.
Andrew Bissell
2
@AndrewBissell J'ai réussi à reprocher vos résultats avec une référence beaucoup plus simple. La branche par rapport au tableau pour les performances de petite taille et de taille moyenne était une supposition quelque peu évidente. Mais je n'ai pas de meilleure idée que quiconque du plongeon dans 30 cas ...
Mysticial

Réponses:

228

Comme l'a souligné l'autre réponse , parce que les valeurs de casse sont contiguës (par opposition à rares), le bytecode généré pour vos différents tests utilise une table de commutation (instruction bytecode tableswitch).

Cependant, une fois que le JIT commence son travail et compile le bytecode dans l'assembly, l' tableswitchinstruction n'aboutit pas toujours à un tableau de pointeurs: parfois la table de commutation est transformée en ce qui ressemble à une lookupswitch(similaire à une structure if/ else if).

La décompilation de l'assembly généré par le JIT (hotspot JDK 1.7) montre qu'il utilise une succession de if / else si quand il y a 17 cas ou moins, un tableau de pointeurs quand il y en a plus de 18 (plus efficace).

La raison pour laquelle ce nombre magique de 18 est utilisé semble se résumer à la valeur par défaut de l' MinJumpTableSizeindicateur JVM (autour de la ligne 352 dans le code).

J'ai soulevé le problème sur la liste des compilateurs de hotspot et il semble que ce soit un héritage de tests passés . Notez que cette valeur par défaut a été supprimée dans JDK 8 après que d' autres analyses comparatives ont été effectuées .

Enfin, lorsque la méthode devient trop longue (> 25 cas dans mes tests), elle n'est plus alignée avec les paramètres JVM par défaut - c'est la cause la plus probable de la baisse des performances à ce stade.


Avec 5 cas, le code décompilé ressemble à ceci (notez les instructions cmp / je / jg / jmp, l'assembly pour if / goto):

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x00000000024f0160: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x00000000024f0167: push   rbp
  0x00000000024f0168: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x00000000024f016c: cmp    edx,0x3
  0x00000000024f016f: je     0x00000000024f01c3
  0x00000000024f0171: cmp    edx,0x3
  0x00000000024f0174: jg     0x00000000024f01a5
  0x00000000024f0176: cmp    edx,0x1
  0x00000000024f0179: je     0x00000000024f019b
  0x00000000024f017b: cmp    edx,0x1
  0x00000000024f017e: jg     0x00000000024f0191
  0x00000000024f0180: test   edx,edx
  0x00000000024f0182: je     0x00000000024f01cb
  0x00000000024f0184: mov    ebp,edx
  0x00000000024f0186: mov    edx,0x17
  0x00000000024f018b: call   0x00000000024c90a0  ; OopMap{off=48}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
                                                ;   {runtime_call}
  0x00000000024f0190: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
  0x00000000024f0191: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffffa7]        # 0x00000000024f0140
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@52 (line 62)
                                                ;   {section_word}
  0x00000000024f0199: jmp    0x00000000024f01cb
  0x00000000024f019b: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff8d]        # 0x00000000024f0130
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@46 (line 60)
                                                ;   {section_word}
  0x00000000024f01a3: jmp    0x00000000024f01cb
  0x00000000024f01a5: cmp    edx,0x5
  0x00000000024f01a8: je     0x00000000024f01b9
  0x00000000024f01aa: cmp    edx,0x5
  0x00000000024f01ad: jg     0x00000000024f0184  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x00000000024f01af: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff81]        # 0x00000000024f0138
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@64 (line 66)
                                                ;   {section_word}
  0x00000000024f01b7: jmp    0x00000000024f01cb
  0x00000000024f01b9: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff67]        # 0x00000000024f0128
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@70 (line 68)
                                                ;   {section_word}
  0x00000000024f01c1: jmp    0x00000000024f01cb
  0x00000000024f01c3: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff55]        # 0x00000000024f0120
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x00000000024f01cb: add    rsp,0x10
  0x00000000024f01cf: pop    rbp
  0x00000000024f01d0: test   DWORD PTR [rip+0xfffffffffdf3fe2a],eax        # 0x0000000000430000
                                                ;   {poll_return}
  0x00000000024f01d6: ret    

Avec 18 cas, l'assemblage ressemble à ceci (notez le tableau de pointeurs qui est utilisé et supprime le besoin de toutes les comparaisons: jmp QWORD PTR [r8+r10*1]saute directement à la bonne multiplication) - c'est la raison probable de l'amélioration des performances:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000287fe20: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x000000000287fe27: push   rbp
  0x000000000287fe28: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000287fe2c: cmp    edx,0x13
  0x000000000287fe2f: jae    0x000000000287fe46
  0x000000000287fe31: movsxd r10,edx
  0x000000000287fe34: shl    r10,0x3
  0x000000000287fe38: movabs r8,0x287fd70       ;   {section_word}
  0x000000000287fe42: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x000000000287fe46: mov    ebp,edx
  0x000000000287fe48: mov    edx,0x31
  0x000000000287fe4d: xchg   ax,ax
  0x000000000287fe4f: call   0x00000000028590a0  ; OopMap{off=52}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
                                                ;   {runtime_call}
  0x000000000287fe54: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
  0x000000000287fe55: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe8b]        # 0x000000000287fce8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@194 (line 92)
                                                ;   {section_word}
  0x000000000287fe5d: jmp    0x000000000287ff16
  0x000000000287fe62: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe86]        # 0x000000000287fcf0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@188 (line 90)
                                                ;   {section_word}
  0x000000000287fe6a: jmp    0x000000000287ff16
  0x000000000287fe6f: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe81]        # 0x000000000287fcf8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@182 (line 88)
                                                ;   {section_word}
  0x000000000287fe77: jmp    0x000000000287ff16
  0x000000000287fe7c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe7c]        # 0x000000000287fd00
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@176 (line 86)
                                                ;   {section_word}
  0x000000000287fe84: jmp    0x000000000287ff16
  0x000000000287fe89: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe77]        # 0x000000000287fd08
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@170 (line 84)
                                                ;   {section_word}
  0x000000000287fe91: jmp    0x000000000287ff16
  0x000000000287fe96: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe72]        # 0x000000000287fd10
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@164 (line 82)
                                                ;   {section_word}
  0x000000000287fe9e: jmp    0x000000000287ff16
  0x000000000287fea0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe70]        # 0x000000000287fd18
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@158 (line 80)
                                                ;   {section_word}
  0x000000000287fea8: jmp    0x000000000287ff16
  0x000000000287feaa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6e]        # 0x000000000287fd20
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@152 (line 78)
                                                ;   {section_word}
  0x000000000287feb2: jmp    0x000000000287ff16
  0x000000000287feb4: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe24]        # 0x000000000287fce0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@146 (line 76)
                                                ;   {section_word}
  0x000000000287febc: jmp    0x000000000287ff16
  0x000000000287febe: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6a]        # 0x000000000287fd30
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@140 (line 74)
                                                ;   {section_word}
  0x000000000287fec6: jmp    0x000000000287ff16
  0x000000000287fec8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe68]        # 0x000000000287fd38
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@134 (line 72)
                                                ;   {section_word}
  0x000000000287fed0: jmp    0x000000000287ff16
  0x000000000287fed2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe66]        # 0x000000000287fd40
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@128 (line 70)
                                                ;   {section_word}
  0x000000000287feda: jmp    0x000000000287ff16
  0x000000000287fedc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe64]        # 0x000000000287fd48
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@122 (line 68)
                                                ;   {section_word}
  0x000000000287fee4: jmp    0x000000000287ff16
  0x000000000287fee6: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe62]        # 0x000000000287fd50
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@116 (line 66)
                                                ;   {section_word}
  0x000000000287feee: jmp    0x000000000287ff16
  0x000000000287fef0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe60]        # 0x000000000287fd58
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@110 (line 64)
                                                ;   {section_word}
  0x000000000287fef8: jmp    0x000000000287ff16
  0x000000000287fefa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5e]        # 0x000000000287fd60
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@104 (line 62)
                                                ;   {section_word}
  0x000000000287ff02: jmp    0x000000000287ff16
  0x000000000287ff04: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5c]        # 0x000000000287fd68
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@98 (line 60)
                                                ;   {section_word}
  0x000000000287ff0c: jmp    0x000000000287ff16
  0x000000000287ff0e: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe12]        # 0x000000000287fd28
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x000000000287ff16: add    rsp,0x10
  0x000000000287ff1a: pop    rbp
  0x000000000287ff1b: test   DWORD PTR [rip+0xfffffffffd9b00df],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x000000000287ff21: ret    

Et enfin, l'assemblage avec 30 cas (ci-dessous) ressemble à 18 cas, à l'exception de celui movapd xmm0,xmm1qui apparaît vers le milieu du code, tel que repéré par @cHao - mais la raison la plus probable de la baisse des performances est que la méthode est trop longtemps pour être aligné avec les paramètres JVM par défaut:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x0000000002524560: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x0000000002524567: push   rbp
  0x0000000002524568: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000252456c: movapd xmm1,xmm0
  0x0000000002524570: cmp    edx,0x1f
  0x0000000002524573: jae    0x0000000002524592  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524575: movsxd r10,edx
  0x0000000002524578: shl    r10,0x3
  0x000000000252457c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe3c]        # 0x00000000025243c0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@364 (line 118)
                                                ;   {section_word}
  0x0000000002524584: movabs r8,0x2524450       ;   {section_word}
  0x000000000252458e: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524592: mov    ebp,edx
  0x0000000002524594: mov    edx,0x31
  0x0000000002524599: xchg   ax,ax
  0x000000000252459b: call   0x00000000024f90a0  ; OopMap{off=64}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
                                                ;   {runtime_call}
  0x00000000025245a0: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
  0x00000000025245a1: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe27]        # 0x00000000025243d0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@358 (line 116)
                                                ;   {section_word}
  0x00000000025245a9: jmp    0x0000000002524744
  0x00000000025245ae: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe22]        # 0x00000000025243d8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@348 (line 114)
                                                ;   {section_word}
  0x00000000025245b6: jmp    0x0000000002524744
  0x00000000025245bb: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe1d]        # 0x00000000025243e0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@338 (line 112)
                                                ;   {section_word}
  0x00000000025245c3: jmp    0x0000000002524744
  0x00000000025245c8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe18]        # 0x00000000025243e8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@328 (line 110)
                                                ;   {section_word}
  0x00000000025245d0: jmp    0x0000000002524744
  0x00000000025245d5: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe13]        # 0x00000000025243f0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@318 (line 108)
                                                ;   {section_word}
  0x00000000025245dd: jmp    0x0000000002524744
  0x00000000025245e2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0e]        # 0x00000000025243f8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@308 (line 106)
                                                ;   {section_word}
  0x00000000025245ea: jmp    0x0000000002524744
  0x00000000025245ef: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe09]        # 0x0000000002524400
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@298 (line 104)
                                                ;   {section_word}
  0x00000000025245f7: jmp    0x0000000002524744
  0x00000000025245fc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe04]        # 0x0000000002524408
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@288 (line 102)
                                                ;   {section_word}
  0x0000000002524604: jmp    0x0000000002524744
  0x0000000002524609: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdff]        # 0x0000000002524410
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@278 (line 100)
                                                ;   {section_word}
  0x0000000002524611: jmp    0x0000000002524744
  0x0000000002524616: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdfa]        # 0x0000000002524418
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@268 (line 98)
                                                ;   {section_word}
  0x000000000252461e: jmp    0x0000000002524744
  0x0000000002524623: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffd9d]        # 0x00000000025243c8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@258 (line 96)
                                                ;   {section_word}
  0x000000000252462b: jmp    0x0000000002524744
  0x0000000002524630: movapd xmm0,xmm1
  0x0000000002524634: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0c]        # 0x0000000002524448
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@242 (line 92)
                                                ;   {section_word}
  0x000000000252463c: jmp    0x0000000002524744
  0x0000000002524641: movapd xmm0,xmm1
  0x0000000002524645: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffddb]        # 0x0000000002524428
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@236 (line 90)
                                                ;   {section_word}
  0x000000000252464d: jmp    0x0000000002524744
  0x0000000002524652: movapd xmm0,xmm1
  0x0000000002524656: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdd2]        # 0x0000000002524430
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@230 (line 88)
                                                ;   {section_word}
  0x000000000252465e: jmp    0x0000000002524744
  0x0000000002524663: movapd xmm0,xmm1
  0x0000000002524667: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdc9]        # 0x0000000002524438
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@224 (line 86)
                                                ;   {section_word}

[etc.]

  0x0000000002524744: add    rsp,0x10
  0x0000000002524748: pop    rbp
  0x0000000002524749: test   DWORD PTR [rip+0xfffffffffde1b8b1],eax        # 0x0000000000340000
                                                ;   {poll_return}
  0x000000000252474f: ret    
assylias
la source
7
@ syb0rg Pour être honnête, je ne comprends pas non plus les détails ;-)
assylias
4
+1 pour une excellente réponse! Pourriez-vous démonter quelque chose avec plus de 30 cas pour comparer lorsque les performances sortent du "creux" dans le tableau de l'OP?
asteri
4
@VivinPaliath stackoverflow.com/questions/1503479/…
assylias
2
@AndrewBissell Je suppose que le comportement différent est basé soit sur (i) des tests de performances inter-architectures qui ont montré que le tableau de pointeurs n'est efficace que lorsque le nombre de cas est supérieur à 18 ou (ii) le code est profilé comme il est exécuté et le profileur détermine quelle approche est la meilleure pendant l'exécution. Je ne trouve pas la réponse.
assylias
3
Le démontage de 30 cas et celui de 18 cas se ressemblent principalement. Les différences semblent principalement limitées à un peu supplémentaire de réarrangement des registres après environ le 11e cas. Je ne peux pas dire pourquoi le JITter fait ça; cela semble inutile.
cHao
46

Switch - case est plus rapide si les valeurs de case sont placées dans une plage étroite.

case 1:
case 2:
case 3:
..
..
case n:

Parce que, dans ce cas, le compilateur peut éviter d'effectuer une comparaison pour chaque jambe de cas dans l'instruction switch. Le compilateur crée une table de sauts qui contient les adresses des actions à entreprendre sur différentes jambes. La valeur sur laquelle le changement est effectué est manipulée pour le convertir en index dans lejump table . Dans cette implémentation, le temps pris dans l'instruction switch est bien inférieur au temps pris dans une cascade d'instructions if-else-if équivalente. Le temps pris dans l'instruction switch est également indépendant du nombre de segments de casse dans l'instruction switch.

Comme indiqué dans wikipedia sur l' instruction switch dans la section Compilation.

Si la plage de valeurs d'entrée est identiquement `` petite '' et ne comporte que quelques lacunes, certains compilateurs qui incorporent un optimiseur peuvent en fait implémenter l'instruction switch comme une table de branche ou un tableau de pointeurs de fonction indexés au lieu d'une longue série d'instructions conditionnelles. Cela permet à l'instruction switch de déterminer instantanément quelle branche exécuter sans avoir à parcourir une liste de comparaisons.

Vishal K
la source
4
ce n'est pas correct. Il sera plus rapide quelles que soient les valeurs de cas étant étroites ou larges. Il s'agit de O (1) - peu importe la différence entre les valeurs de casse.
Aniket Inge
6
@Aniket: Lisez cet article de wikipedia. en.wikipedia.org/wiki/Branch_table
Vishal K
14
@Aniket: Ce n'est pas O (1) si la plage est large et clairsemée. Il existe deux types de commutateurs, et si la plage est trop étendue, Java la compilera en un "lookupswitch" plutôt qu'en un "tableswitch". Le premier nécessite une comparaison par branche jusqu'à ce que l'on trouve, tandis que le second ne le fait pas.
cHao
4
Wikipedia est un endroit décent pour trouver des références, mais ne doit pas être considéré comme une source faisant autorité. Tout ce que vous y lisez est au mieux une information de seconde main.
cHao
6
@Aniket: En toute honnêteté, le démontage est spécifique à une machine virtuelle Java donnée sur une plateforme spécifique. D'autres peuvent le traduire différemment. Certains pourraient en fait utiliser une table de hachage pour un commutateur de recherche. Il ne fonctionnera toujours pas aussi bien qu'un interrupteur de table, mais il pourrait au moins être proche. Cela prendrait plus de temps pour JIT et impliquerait d'appliquer un algorithme de hachage à l'entrée. Ainsi, bien que le code d'assemblage résultant puisse être instructif, il ne fait pas autorité non plus, sauf si vous parlez spécifiquement de Hotspot v1.7, quoi que sous Windows x86_64.
cHao
30

La réponse réside dans le bytecode:

SwitchTest10.java

public class SwitchTest10 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 10: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Bytecode correspondant; seules les pièces pertinentes sont affichées:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 10
        0: 60;
        1: 70;
        2: 80;
        3: 90;
        4: 100;
        5: 110;
        6: 120;
        7: 131;
        8: 142;
        9: 153;
        10: 164;
        default: 175 }

SwitchTest22.java:

public class SwitchTest22 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 100: System.out.println(10);
                    break;

            case 110: System.out.println(10);
                    break;
            case 120: System.out.println(10);
                    break;
            case 130: System.out.println(10);
                    break;
            case 140: System.out.println(10);
                    break;
            case 150: System.out.println(10);
                    break;
            case 160: System.out.println(10);
                    break;
            case 170: System.out.println(10);
                    break;
            case 180: System.out.println(10);
                    break;
            case 190: System.out.println(10);
                    break;
            case 200: System.out.println(10);
                    break;
            case 210: System.out.println(10);
                    break;

            case 220: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Bytecode correspondant; encore une fois, seules les parties pertinentes sont affichées:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   lookupswitch{ //23
        0: 196;
        1: 206;
        2: 216;
        3: 226;
        4: 236;
        5: 246;
        6: 256;
        7: 267;
        8: 278;
        9: 289;
        100: 300;
        110: 311;
        120: 322;
        130: 333;
        140: 344;
        150: 355;
        160: 366;
        170: 377;
        180: 388;
        190: 399;
        200: 410;
        210: 421;
        220: 432;
        default: 443 }

Dans le premier cas, avec des plages étroites, le bytecode compilé utilise a tableswitch. Dans le second cas, le bytecode compilé utilise a lookupswitch.

Dans tableswitch, la valeur entière en haut de la pile est utilisée pour indexer dans la table, pour trouver la branche / saut cible. Ce saut / branche est alors effectué immédiatement. Il s'agit donc d'une O(1)opération.

A lookupswitchest plus compliqué. Dans ce cas, la valeur entière doit être comparée à toutes les clés du tableau jusqu'à ce que la clé correcte soit trouvée. Une fois la clé trouvée, la cible de branche / saut (à laquelle cette clé est mappée) est utilisée pour le saut. La table utilisée dans lookupswitchest triée et un algorithme de recherche binaire peut être utilisé pour trouver la clé correcte. Les performances pour une recherche binaire sont O(log n), et tout le processus l'est également O(log n), car le saut est toujours O(1). Ainsi, la raison pour laquelle les performances sont inférieures dans le cas de plages clairsemées est que la clé correcte doit d'abord être recherchée car vous ne pouvez pas indexer directement dans la table.

S'il y a des valeurs éparses et que vous n'aviez qu'une tableswitchà utiliser, la table contiendrait essentiellement des entrées factices qui pointent vers l' defaultoption. Par exemple, en supposant que la dernière entrée dans SwitchTest10.javaétait à la 21place de 10, vous obtenez:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 21
        0: 104;
        1: 114;
        2: 124;
        3: 134;
        4: 144;
        5: 154;
        6: 164;
        7: 175;
        8: 186;
        9: 197;
        10: 219;
        11: 219;
        12: 219;
        13: 219;
        14: 219;
        15: 219;
        16: 219;
        17: 219;
        18: 219;
        19: 219;
        20: 219;
        21: 208;
        default: 219 }

Ainsi, le compilateur crée essentiellement cette immense table contenant des entrées factices entre les espaces, pointant vers la branche cible de l' defaultinstruction. Même s'il n'y en a pas default, il contiendra des entrées pointant vers l'instruction après le bloc de commutation. J'ai fait quelques tests de base, et j'ai trouvé que si l'écart entre le dernier index et le précédent ( 9) est plus grand que 35, il utilise a lookupswitchau lieu de a tableswitch.

Le comportement de l' switchinstruction est défini dans Java Virtual Machine Specification (§3.10) :

Lorsque les cas du commutateur sont rares, la représentation sous forme de table de l'instruction de commutateur de table devient inefficace en termes d'espace. L'instruction lookupswitch peut être utilisée à la place. L'instruction lookupswitch associe les clés int (les valeurs des étiquettes de cas) aux décalages cibles dans une table. Lorsqu'une instruction lookupswitch est exécutée, la valeur de l'expression du commutateur est comparée aux clés de la table. Si l'une des clés correspond à la valeur de l'expression, l'exécution se poursuit au décalage cible associé. Si aucune clé ne correspond, l'exécution se poursuit sur la cible par défaut. [...]

Vivin Paliath
la source
1
J'ai compris de la question que les nombres sont toujours contigus mais la plage est plus ou moins longue - c'est-à-dire que dans un exemple les cas vont de 0 à 5 alors que dans un autre exemple ils vont de 0 à 30 - et aucun des exemples n'utilise des valeurs éparses
assylias
@assylias Hmm, intéressant. Je suppose que j'ai mal compris la question. Permettez-moi de faire plus d'expérimentation. Vous dites donc que même avec une plage contiguë de 0 à 30, le compilateur utilise un lookupswitch?
Vivin Paliath
@VivinPaliath: Oui, dans mes tests, les constantes de casse sont toujours contiguës, donc je teste essentiellement les commutateurs sur [0, 1], [0, 1, 2], [0, 1, 2, 3] ... etc
Andrew Bissell
@VivinPaliath Non, le bytecode utilise toujours un commutateur de table - cependant le compilateur JIT ne semble pas compiler le commutateur de table à assembler de la même manière selon le nombre d'éléments qu'il contient.
assylias
6
@VivinPaliath J'aurais pu formuler la question plus clairement à coup sûr. Je suis en quelque sorte hors de ma profondeur en ce qui concerne l'évaluation des réponses impliquant ce truc de bytecode et d'assemblage de bas niveau. Il me semble toujours que la distinction tableswitch / lookupswitch est réellement importante ici, et la vôtre est la seule réponse qui utilise ces termes jusqu'à présent (bien que les autres énoncent probablement le même concept avec une terminologie différente). De plus, j'aime aussi avoir le lien JVM Spec.
Andrew Bissell
19

Puisque la question a déjà répondu (plus ou moins), voici quelques conseils. Utilisation

private static final double[] mul={1d, 10d...};
static double multiplyByPowerOfTen(final double d, final int exponent) {
      if (exponent<0 || exponent>=mul.length) throw new ParseException();//or just leave the IOOBE be
      return mul[exponent]*d;
}

Ce code utilise beaucoup moins IC (cache d'instructions) et sera toujours intégré. Le tableau sera dans le cache de données L1 si le code est chaud. La table de recherche est presque toujours une victoire. (en particulier sur les repères microbiens: D)

Modifier: si vous souhaitez que la méthode soit hot-inline, considérez que les chemins non rapides aiment throw new ParseException()être aussi courts que minimum ou déplacez-les vers une méthode statique distincte (ce qui les rend courts comme minimum). C'est throw new ParseException("Unhandled power of ten " + power, 0);une idée faible car elle mange une grande partie du budget en ligne pour le code qui peut être simplement interprété - la concaténation de chaînes est assez verbeuse en bytecode. Plus d'infos et un vrai cas avec ArrayList

bestsss
la source