web-dev-qa-db-fra.com

En Java, peut & être plus rapide que &&?

Dans ce code:

if (value >= x && value <= y) {

lorsque value >= x et value <= y sont vraisemblablement vrais ou faux sans motif particulier, l'utilisation de l'opérateur & serait-elle plus rapide que l'utilisation de &&?

Plus précisément, je pense à la façon dont && Évalue paresseusement l'expression de droite (c'est-à-dire uniquement si le LHS est vrai), ce qui implique un conditionnel, alors qu'en Java & Dans ce contexte garantit une évaluation stricte des deux sous-expressions (booléennes). Le résultat de la valeur est le même dans les deux cas.

Mais alors qu'un opérateur >= Ou <= Utilisera une instruction de comparaison simple, && Doit impliquer une branche, et cette branche est susceptible d'échec de prédiction de branche - selon cette question très célèbre: Pourquoi est-il plus rapide de traiter un tableau trié qu'un tableau non trié?

Donc, forcer l'expression à ne pas avoir de composants paresseux sera sûrement plus déterministe et ne sera pas vulnérable à l'échec de la prédiction. Droite?

Remarques:

  • évidemment la réponse à ma question serait Non si le code ressemblait à ceci: if(value >= x && verySlowFunction()). Je me concentre sur les expressions RHS "suffisamment simples".
  • il y a quand même une branche conditionnelle (l'instruction if). Je ne peux pas tout à fait me prouver que cela n'est pas pertinent et que des formulations alternatives pourraient être de meilleurs exemples, comme boolean b = value >= x && value <= y;
  • tout cela tombe dans le monde des micro-optimisations horribles. Ouais, je sais :-) ... intéressant quand même?

Mise à jour Juste pour expliquer pourquoi je suis intéressé: j'ai regardé les systèmes sur lesquels Martin Thompson a écrit sur son Mechanical Blog de sympathie , après qu'il soit venu et a fait un discours sur Aeron. L'un des messages clés est que notre matériel contient tous ces éléments magiques, et nous, développeurs de logiciels, ne pouvons tragiquement pas en tirer parti. Ne vous inquiétez pas, je ne suis pas sur le point de s/&&/\ &/sur tout mon code :-) ... mais il y a un certain nombre de questions sur ce site sur l'amélioration de la prédiction des branches en supprimant les branches, et cela s'est produit pour moi, les opérateurs booléens conditionnels sont au cœur des conditions de test.

Bien sûr, @StephenC fait le point fantastique que le fait de plier votre code en formes étranges peut rendre moins facile pour les JIT de repérer les optimisations communes - sinon maintenant, puis à l'avenir. Et que la question très célèbre mentionnée ci-dessus est spéciale car elle pousse la complexité de la prédiction bien au-delà de l'optimisation pratique.

Je suis à peu près conscient que dans la plupart des situations (ou presque toutes), && Est la meilleure chose à faire, la plus claire, la plus simple, la plus rapide - même si je suis très reconnaissante aux personnes qui ont posté des réponses le démontrant! Je suis vraiment intéressé de voir s'il y a réellement des cas dans l'expérience de quelqu'un où la réponse à "& Peut-elle être plus rapide?" pourrait être Oui ...

Mise à jour 2 : (Répondre aux conseils que la question est trop large. Je ne veux pas apporter de changements majeurs à cette question car elle pourrait compromettre certaines des réponses ci-dessous, qui sont d'une qualité exceptionnelle!) Peut-être un exemple dans la nature est nécessaire; cela vient de la classe Guava LongMath (merci énormément à @maaartinus de l'avoir trouvé):

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

Voir d'abord &? Et si vous vérifiez le lien, la méthode next est appelée lessThanBranchFree(...), ce qui laisse entendre que nous sommes en territoire d'évitement des branches - et la goyave est vraiment très utilisée: chaque cycle est enregistré fait baisser visiblement le niveau de la mer. Posons donc la question de cette façon: cette utilisation de & (Où && Serait plus normal) est-elle une véritable optimisation?

69
SusanW

Ok, donc vous voulez savoir comment il se comporte au niveau inférieur ... Jetons un coup d'œil au bytecode alors!

EDIT: ajout du code assembleur généré pour AMD64, à la fin. Jetez un œil à quelques notes intéressantes.
EDIT 2 (re: OP "Update 2"): ajout du code asm pour méthode isPowerOfTwo de Guava également.

Source Java

J'ai écrit ces deux méthodes rapides:

public boolean AndSC(int x, int value, int y) {
    return value >= x && value <= y;
}

public boolean AndNonSC(int x, int value, int y) {
    return value >= x & value <= y;
}

Comme vous pouvez le voir, ils sont exactement les mêmes, sauf pour le type d'opérateur ET.

Bytecode Java

Et voici le bytecode généré:

  public AndSC(III)Z
   L0
    LINENUMBER 8 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 9 L2
    ICONST_1
    IRETURN
   L1
    LINENUMBER 11 L1
   FRAME SAME
    ICONST_0
    IRETURN
   L3
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
    LOCALVARIABLE x I L0 L3 1
    LOCALVARIABLE value I L0 L3 2
    LOCALVARIABLE y I L0 L3 3
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1
  public AndNonSC(III)Z
   L0
    LINENUMBER 15 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ICONST_1
    GOTO L2
   L1
   FRAME SAME
    ICONST_0
   L2
   FRAME SAME1 I
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L3
    ICONST_1
    GOTO L4
   L3
   FRAME SAME1 I
    ICONST_0
   L4
   FRAME FULL [test/lsoto/AndTest I I I] [I I]
    IAND
    IFEQ L5
   L6
    LINENUMBER 16 L6
    ICONST_1
    IRETURN
   L5
    LINENUMBER 18 L5
   FRAME SAME
    ICONST_0
    IRETURN
   L7
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
    LOCALVARIABLE x I L0 L7 1
    LOCALVARIABLE value I L0 L7 2
    LOCALVARIABLE y I L0 L7 3
    MAXSTACK = 3
    MAXLOCALS = 4

La méthode AndSC (&&) Génère deux sauts conditionnels, comme prévu:

  1. Il charge value et x sur la pile et passe à L1 si value est inférieur. Sinon, il continue à exécuter les lignes suivantes.
  2. Il charge value et y sur la pile, et passe également à L1, si value est supérieur. Sinon, il continue à exécuter les lignes suivantes.
  3. Ce qui se trouve être un return true Au cas où aucun des deux sauts n'aurait été effectué.
  4. Et puis nous avons les lignes marquées comme L1 qui sont un return false.

Cependant, la méthode AndNonSC (&) Génère trois sauts conditionnels!

  1. Il charge value et x sur la pile et passe à L1 si value est inférieur. Parce qu'il doit maintenant enregistrer le résultat pour le comparer avec l'autre partie de l'AND, il doit donc exécuter soit "save true" ou "save false", il ne peut pas faire les deux avec la même instruction.
  2. Il charge value et y sur la pile et passe à L1 si value est supérieur. Encore une fois, il doit enregistrer true ou false et ce sont deux lignes différentes selon le résultat de la comparaison.
  3. Maintenant que les deux les comparaisons sont faites, le code exécute réellement l'opération AND - et si les deux sont vrais, il saute (pour une troisième fois) pour retourner vrai; ou bien il continue son exécution sur la ligne suivante pour retourner false.

(Préliminaire) Conclusion

Bien que je ne sois pas très expérimenté avec Java bytecode et j'ai peut-être oublié quelque chose, il me semble que & Exécutera réellement pire que && dans tous les cas: il génère plus d'instructions à exécuter, y compris plus de sauts conditionnels à prévoir et éventuellement à échouer.

Une réécriture du code pour remplacer les comparaisons par des opérations arithmétiques, comme quelqu'un l'a proposé, pourrait être un moyen de faire de & Une meilleure option, mais au prix de rendre le code beaucoup moins clair.
À mon humble avis, cela ne vaut pas la peine pour 99% des scénarios (cela peut en valoir la peine pour les boucles de 1% qui doivent être extrêmement optimisées, cependant).

EDIT: Assemblée AMD64

Comme indiqué dans les commentaires, le même Java bytecode peut conduire à un code machine différent dans différents systèmes, donc alors que le Java bytecode pourrait nous donner un indice sur lequel La version AND fonctionne mieux, obtenir l'ASM réel tel que généré par le compilateur est le seul moyen de vraiment le savoir.
J'ai imprimé les instructions AMD64 ASM pour les deux méthodes; ci-dessous sont les lignes pertinentes (points d'entrée dépouillés, etc.).

REMARQUE: toutes les méthodes compilées avec Java 1.8.0_91 sauf indication contraire.

Méthode AndSC avec options par défaut

  # {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923e3e: cmp    %r8d,%r9d
  0x0000000002923e41: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e4b: movabs $0x108,%rsi
  0x0000000002923e55: jl     0x0000000002923e65
  0x0000000002923e5b: movabs $0x118,%rsi
  0x0000000002923e65: mov    (%rax,%rsi,1),%rbx
  0x0000000002923e69: lea    0x1(%rbx),%rbx
  0x0000000002923e6d: mov    %rbx,(%rax,%rsi,1)
  0x0000000002923e71: jl     0x0000000002923eb0  ;*if_icmplt
                                                ; - AndTest::AndSC@2 (line 22)

  0x0000000002923e77: cmp    %edi,%r9d
  0x0000000002923e7a: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e84: movabs $0x128,%rsi
  0x0000000002923e8e: jg     0x0000000002923e9e
  0x0000000002923e94: movabs $0x138,%rsi
  0x0000000002923e9e: mov    (%rax,%rsi,1),%rdi
  0x0000000002923ea2: lea    0x1(%rdi),%rdi
  0x0000000002923ea6: mov    %rdi,(%rax,%rsi,1)
  0x0000000002923eaa: jle    0x0000000002923ec1  ;*if_icmpgt
                                                ; - AndTest::AndSC@7 (line 22)

  0x0000000002923eb0: mov    $0x0,%eax
  0x0000000002923eb5: add    $0x30,%rsp
  0x0000000002923eb9: pop    %rbp
  0x0000000002923eba: test   %eax,-0x1c73dc0(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ec0: retq                      ;*ireturn
                                                ; - AndTest::AndSC@13 (line 25)

  0x0000000002923ec1: mov    $0x1,%eax
  0x0000000002923ec6: add    $0x30,%rsp
  0x0000000002923eca: pop    %rbp
  0x0000000002923ecb: test   %eax,-0x1c73dd1(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ed1: retq   

Méthode AndSC avec l'option -XX:PrintAssemblyOptions=intel

  # {method} {0x00000000170a0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c26e2c: cmp    r9d,r8d
  0x0000000002c26e2f: jl     0x0000000002c26e36  ;*if_icmplt
  0x0000000002c26e31: cmp    r9d,edi
  0x0000000002c26e34: jle    0x0000000002c26e44  ;*iconst_0
  0x0000000002c26e36: xor    eax,eax            ;*synchronization entry
  0x0000000002c26e38: add    rsp,0x10
  0x0000000002c26e3c: pop    rbp
  0x0000000002c26e3d: test   DWORD PTR [rip+0xffffffffffce91bd],eax        # 0x0000000002910000
  0x0000000002c26e43: ret    
  0x0000000002c26e44: mov    eax,0x1
  0x0000000002c26e49: jmp    0x0000000002c26e38

Méthode AndNonSC avec options par défaut

  # {method} {0x0000000016da0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923a78: cmp    %r8d,%r9d
  0x0000000002923a7b: mov    $0x0,%eax
  0x0000000002923a80: jl     0x0000000002923a8b
  0x0000000002923a86: mov    $0x1,%eax
  0x0000000002923a8b: cmp    %edi,%r9d
  0x0000000002923a8e: mov    $0x0,%esi
  0x0000000002923a93: jg     0x0000000002923a9e
  0x0000000002923a99: mov    $0x1,%esi
  0x0000000002923a9e: and    %rsi,%rax
  0x0000000002923aa1: cmp    $0x0,%eax
  0x0000000002923aa4: je     0x0000000002923abb  ;*ifeq
                                                ; - AndTest::AndNonSC@21 (line 29)

  0x0000000002923aaa: mov    $0x1,%eax
  0x0000000002923aaf: add    $0x30,%rsp
  0x0000000002923ab3: pop    %rbp
  0x0000000002923ab4: test   %eax,-0x1c739ba(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923aba: retq                      ;*ireturn
                                                ; - AndTest::AndNonSC@25 (line 30)

  0x0000000002923abb: mov    $0x0,%eax
  0x0000000002923ac0: add    $0x30,%rsp
  0x0000000002923ac4: pop    %rbp
  0x0000000002923ac5: test   %eax,-0x1c739cb(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923acb: retq   

Méthode AndNonSC avec l'option -XX:PrintAssemblyOptions=intel

  # {method} {0x00000000170a0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c270b5: cmp    r9d,r8d
  0x0000000002c270b8: jl     0x0000000002c270df  ;*if_icmplt
  0x0000000002c270ba: mov    r8d,0x1            ;*iload_2
  0x0000000002c270c0: cmp    r9d,edi
  0x0000000002c270c3: cmovg  r11d,r10d
  0x0000000002c270c7: and    r8d,r11d
  0x0000000002c270ca: test   r8d,r8d
  0x0000000002c270cd: setne  al
  0x0000000002c270d0: movzx  eax,al
  0x0000000002c270d3: add    rsp,0x10
  0x0000000002c270d7: pop    rbp
  0x0000000002c270d8: test   DWORD PTR [rip+0xffffffffffce8f22],eax        # 0x0000000002910000
  0x0000000002c270de: ret    
  0x0000000002c270df: xor    r8d,r8d
  0x0000000002c270e2: jmp    0x0000000002c270c0
  • Tout d'abord, le code ASM généré diffère selon que nous choisissons la syntaxe AT&T par défaut ou la syntaxe Intel.
  • Avec la syntaxe AT&T:
    • Le code ASM est en fait plus long pour la méthode AndSC, avec chaque bytecode IF_ICMP* Traduit en deux instructions de saut d'assemblage, pour un total de 4 sauts conditionnels.
    • Pendant ce temps, pour la méthode AndNonSC, le compilateur génère un code plus simple, où chaque bytecode IF_ICMP* Est traduit en une seule instruction de saut d'assemblage, en conservant le nombre d'origine de 3 sauts conditionnels.
  • Avec la syntaxe Intel:
    • Le code ASM pour AndSC est plus court, avec seulement 2 sauts conditionnels (sans compter les jmp non conditionnels à la fin). En fait, ce n'est que deux CMP, deux JL/E et un XOR/MOV selon le résultat.
    • Le code ASM pour AndNonSC est maintenant plus long que celui de AndSC! Cependant , il n'a qu'un saut conditionnel (pour la première comparaison), en utilisant les registres pour comparer directement le premier résultat avec le second, sans plus de sauts .

Conclusion après analyse du code ASM

  • Au niveau du langage machine AMD64, l'opérateur & Semble générer du code ASM avec moins de sauts conditionnels, ce qui pourrait être meilleur pour les taux d'échec de prédiction élevés (aléatoire values par exemple).
  • D'un autre côté, l'opérateur && Semble générer du code ASM avec moins d'instructions (avec l'option -XX:PrintAssemblyOptions=intel De toute façon), ce qui pourrait être mieux pour vraiment long des boucles avec des entrées adaptées aux prédictions, où le moins de cycles CPU pour chaque comparaison peut faire une différence à long terme.

Comme je l'ai dit dans certains commentaires, cela va varier considérablement entre les systèmes, donc si nous parlons d'optimisation de prédiction de branche, la seule vraie réponse serait: cela dépend de votre Implémentation JVM, votre compilateur, votre CPU et vos données d'entrée .


Addendum: la méthode isPowerOfTwo de Guava

Ici, les développeurs de Guava ont trouvé une façon astucieuse de calculer si un nombre donné est une puissance de 2:

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

OP de citation:

cette utilisation de & (où && serait plus normal) est-elle une véritable optimisation?

Pour savoir si c'est le cas, j'ai ajouté deux méthodes similaires à ma classe de test:

public boolean isPowerOfTwoAND(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

public boolean isPowerOfTwoANDAND(long x) {
    return x > 0 && (x & (x - 1)) == 0;
}

Code ASM d'Intel pour la version de Guava

  # {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103bbe: movabs rax,0x0
  0x0000000003103bc8: cmp    rax,r8
  0x0000000003103bcb: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103bd5: movabs rsi,0x108
  0x0000000003103bdf: jge    0x0000000003103bef
  0x0000000003103be5: movabs rsi,0x118
  0x0000000003103bef: mov    rdi,QWORD PTR [rax+rsi*1]
  0x0000000003103bf3: lea    rdi,[rdi+0x1]
  0x0000000003103bf7: mov    QWORD PTR [rax+rsi*1],rdi
  0x0000000003103bfb: jge    0x0000000003103c1b  ;*lcmp
  0x0000000003103c01: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c0b: inc    DWORD PTR [rax+0x128]
  0x0000000003103c11: mov    eax,0x1
  0x0000000003103c16: jmp    0x0000000003103c20  ;*goto
  0x0000000003103c1b: mov    eax,0x0            ;*lload_1
  0x0000000003103c20: mov    rsi,r8
  0x0000000003103c23: movabs r10,0x1
  0x0000000003103c2d: sub    rsi,r10
  0x0000000003103c30: and    rsi,r8
  0x0000000003103c33: movabs rdi,0x0
  0x0000000003103c3d: cmp    rsi,rdi
  0x0000000003103c40: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c4a: movabs rdi,0x140
  0x0000000003103c54: jne    0x0000000003103c64
  0x0000000003103c5a: movabs rdi,0x150
  0x0000000003103c64: mov    rbx,QWORD PTR [rsi+rdi*1]
  0x0000000003103c68: lea    rbx,[rbx+0x1]
  0x0000000003103c6c: mov    QWORD PTR [rsi+rdi*1],rbx
  0x0000000003103c70: jne    0x0000000003103c90  ;*lcmp
  0x0000000003103c76: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c80: inc    DWORD PTR [rsi+0x160]
  0x0000000003103c86: mov    esi,0x1
  0x0000000003103c8b: jmp    0x0000000003103c95  ;*goto
  0x0000000003103c90: mov    esi,0x0            ;*iand
  0x0000000003103c95: and    rsi,rax
  0x0000000003103c98: and    esi,0x1
  0x0000000003103c9b: mov    rax,rsi
  0x0000000003103c9e: add    rsp,0x50
  0x0000000003103ca2: pop    rbp
  0x0000000003103ca3: test   DWORD PTR [rip+0xfffffffffe44c457],eax        # 0x0000000001550100
  0x0000000003103ca9: ret    

Code asm d'Intel pour la version &&

  # {method} {0x0000000017580bd0} 'isPowerOfTwoANDAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103438: movabs rax,0x0
  0x0000000003103442: cmp    rax,r8
  0x0000000003103445: jge    0x0000000003103471  ;*lcmp
  0x000000000310344b: mov    rax,r8
  0x000000000310344e: movabs r10,0x1
  0x0000000003103458: sub    rax,r10
  0x000000000310345b: and    rax,r8
  0x000000000310345e: movabs rsi,0x0
  0x0000000003103468: cmp    rax,rsi
  0x000000000310346b: je     0x000000000310347b  ;*lcmp
  0x0000000003103471: mov    eax,0x0
  0x0000000003103476: jmp    0x0000000003103480  ;*ireturn
  0x000000000310347b: mov    eax,0x1            ;*goto
  0x0000000003103480: and    eax,0x1
  0x0000000003103483: add    rsp,0x40
  0x0000000003103487: pop    rbp
  0x0000000003103488: test   DWORD PTR [rip+0xfffffffffe44cc72],eax        # 0x0000000001550100
  0x000000000310348e: ret    

Dans cet exemple spécifique, le compilateur JIT génère loin moins de code d'assemblage pour la version && Que pour la version & De Guava (et, après les résultats d'hier, j'étais honnêtement surpris par cela).
Par rapport à Guava, la version && Se traduit par 25% de bytecode en moins pour la compilation de JIT, 50% moins d'instructions d'assemblage et seulement deux sauts conditionnels (la version & En a quatre d'eux).

Donc, tout indique que la méthode & De Guava est moins efficace que la version plus "naturelle" &&.

... Ou est-ce?

Comme indiqué précédemment, j'exécute les exemples ci-dessus avec Java 8:

C:\....>Java -version
Java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

Mais et si je passe à Java 7 ?

C:\....>c:\jdk1.7.0_79\bin\Java -version
Java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
C:\....>c:\jdk1.7.0_79\bin\Java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain
  .....
  0x0000000002512bac: xor    r10d,r10d
  0x0000000002512baf: mov    r11d,0x1
  0x0000000002512bb5: test   r8,r8
  0x0000000002512bb8: jle    0x0000000002512bde  ;*ifle
  0x0000000002512bba: mov    eax,0x1            ;*lload_1
  0x0000000002512bbf: mov    r9,r8
  0x0000000002512bc2: dec    r9
  0x0000000002512bc5: and    r9,r8
  0x0000000002512bc8: test   r9,r9
  0x0000000002512bcb: cmovne r11d,r10d
  0x0000000002512bcf: and    eax,r11d           ;*iand
  0x0000000002512bd2: add    rsp,0x10
  0x0000000002512bd6: pop    rbp
  0x0000000002512bd7: test   DWORD PTR [rip+0xffffffffffc0d423],eax        # 0x0000000002120000
  0x0000000002512bdd: ret    
  0x0000000002512bde: xor    eax,eax
  0x0000000002512be0: jmp    0x0000000002512bbf
  .....

Surprise! Le code d'assembly généré pour la méthode & Par le compilateur JIT dans Java 7, a seulement un saut conditionnel maintenant, et est beaucoup plus court ! Alors que la méthode && (Vous devrez me faire confiance sur celui-ci, je ne veux pas encombrer la fin!) Reste à peu près la même, avec ses deux sauts conditionnels et quelques instructions moins, tops.
On dirait que les ingénieurs de Guava savaient ce qu'ils faisaient, après tout! (s'ils essayaient d'optimiser Java 7 temps d'exécution, c'est ;-)

Revenons donc à la dernière question d'OP:

cette utilisation de & (où && serait plus normal) est-elle une véritable optimisation?

Et à mon humble avis la réponse est la même , même pour ce scénario (très!) Spécifique: cela dépend de votre JVM implémentation, votre compilateur, votre CPU et vos données d'entrée .

72
walen

Pour ce genre de questions, vous devez exécuter un test de performance. J'ai utilisé JMH pour ce test.

Les repères sont mis en œuvre comme

// boolean logical AND
bh.consume(value >= x & y <= value);

et

// conditional AND
bh.consume(value >= x && y <= value);

et

// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)

Avec des valeurs pour value, x and y selon le nom de référence.

Le résultat (cinq échauffements et dix itérations de mesure) pour l'analyse comparative de débit est:

Benchmark                                 Mode  Cnt    Score    Error   Units
Benchmark.isBooleanANDBelowRange          thrpt   10  386.086 ▒ 17.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.240 ▒  7.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.847 ▒ 15.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.877 ▒ 11.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.743 ▒ 15.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.524 ▒ 16.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.190 ▒ 19.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.094 ▒ 15.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.913 ▒  5.537  ops/us

Le résultat n'est pas si différent pour l'évaluation elle-même. Tant qu'aucun impact de performance n'est repéré sur ce morceau de code, je n'essayerais pas de l'optimiser. Selon la place dans le code, le compilateur de hotspot peut décider de faire une optimisation. Ce qui n'est probablement pas couvert par les références ci-dessus.

quelques références:

booléen ET logique - la valeur du résultat est true si les deux valeurs d'opérande sont true; sinon, le résultat est false
ET conditionnel - est comme &, mais évalue son opérande de droite uniquement si la valeur de son opérande de gauche est true
OR au niveau du bit - la valeur de résultat est l'inclusion au niveau du bit OR des valeurs d'opérande

23
SubOptimal

Je vais aborder cela sous un angle différent.

Considérez ces deux fragments de code,

  if (value >= x && value <= y) {

et

  if (value >= x & value <= y) {

Si nous supposons que value, x, y ont un type primitif, ces deux instructions (partielles) donneront le même résultat pour toutes les valeurs d'entrée possibles. (Si des types d'encapsuleur sont impliqués, ils ne sont pas exactement équivalents en raison d'un test implicite null pour y qui pourrait échouer dans le & la version et non la && version.)

Si le compilateur JIT fait du bon travail, son optimiseur pourra déduire que ces deux instructions font la même chose:

  • Si l'un est plus rapide que l'autre, il devrait pouvoir utiliser la version la plus rapide ... dans le code compilé JIT.

  • Sinon, peu importe la version utilisée au niveau du code source.

  • Étant donné que le compilateur JIT rassemble les statistiques de chemin avant la compilation, il peut potentiellement avoir plus d'informations sur les caractéristiques d'exécution que le programmeur (!).

  • Si le compilateur JIT de génération actuelle (sur une plate-forme donnée) n'optimise pas assez bien pour gérer cela, la prochaine génération pourrait bien le faire ... selon que des preuves empiriques indiquent ou non que cela est un tile = motif à optimiser.

  • En effet, si vous vous écrivez Java d'une manière qui optimise cela, il y a ne chance qu'en choisissant la version la plus "obscure" du code, vous pourrait inhiber la capacité du compilateur JIT actuel ou futur à optimiser.

En bref, je ne pense pas que vous devriez faire ce genre de micro-optimisation au niveau du code source. Et si vous acceptez cet argument1, et suivez-le jusqu'à sa conclusion logique, la question de savoir quelle version est la plus rapide est ... théorique2.

1 - Je ne prétends pas que cela soit loin d'être une preuve.

2 - Sauf si vous faites partie de la petite communauté de personnes qui écrivent réellement Java compilateurs JIT ...


La "question très célèbre" est intéressante à deux égards:

  • D'une part, c'est un exemple où le type d'optimisation requis pour faire la différence est bien au-delà de la capacité d'un compilateur JIT.

  • D'un autre côté, ce ne serait pas nécessairement la bonne chose de trier le tableau ... juste parce qu'un tableau trié peut être traité plus rapidement. Le coût du tri du tableau pourrait bien être (beaucoup) supérieur à l'économie.

12
Stephen C

Utilisation de & ou && nécessite encore une condition à évaluer, il est donc peu probable que cela économise du temps de traitement - cela pourrait même y ajouter étant donné que vous évaluez les deux expressions lorsque vous n'avez besoin que d'en évaluer une.

En utilisant & plus de && pour économiser une nanoseconde si cela est inutile dans certaines situations très rares, vous avez déjà perdu plus de temps à envisager la différence que vous n'en auriez économisé en utilisant & plus de &&.

Modifier

Je suis devenu curieux et j'ai décidé de faire quelques repères.

J'ai fait ce cours:

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        runWithOneAnd(30);
        runWithTwoAnds(30);
    }

    static void runWithOneAnd(int value){
        if(value >= x & value <= y){

        }
    }

    static void runWithTwoAnds(int value){
        if(value >= x && value <= y){

        }
    }
}

et a exécuté des tests de profilage avec NetBeans. Je n'ai pas utilisé d'instructions d'impression pour gagner du temps de traitement, sachez que les deux sont évaluées en true.

Premier test:

The first profiling test

Deuxième test:

The second profiling test

Troisième test:

The third profiling test

Comme vous pouvez le voir par les tests de profilage, en utilisant un seul & l'exécution prend en réalité 2 à 3 fois plus de temps que l'utilisation de deux &&. Cela semble aussi étrange que je m'attendais à de meilleures performances d'un seul &.

Je ne suis pas sûr à 100% pourquoi. Dans les deux cas, les deux expressions doivent être évaluées car les deux sont vraies. Je soupçonne que la JVM fait une optimisation spéciale dans les coulisses pour l'accélérer.

Morale de l'histoire: la convention est bonne et l'optimisation prématurée est mauvaise.


Édition 2

J'ai refait le code de référence en gardant à l'esprit les commentaires de @ SvetlinZarev et quelques autres améliorations. Voici le code de référence modifié:

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        oneAndBothTrue();
        oneAndOneTrue();
        oneAndBothFalse();
        twoAndsBothTrue();
        twoAndsOneTrue();
        twoAndsBothFalse();
        System.out.println(b);
    }

    static void oneAndBothTrue() {
        int value = 30;
        for (int i = 0; i < 2000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothTrue() {
        int value = 30;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    //I wanted to avoid print statements here as they can
    //affect the benchmark results. 
    static StringBuilder b = new StringBuilder();
    static int times = 0;

    static void doSomething(){
        times++;
        b.append("I have run ").append(times).append(" times \n");
    }
}

Et voici les tests de performances:

Test 1:

enter image description here

Test 2:

enter image description here

Test 3:

enter image description here

Cela prend également en compte différentes valeurs et différentes conditions.

Utiliser un & prend plus de temps à s'exécuter lorsque les deux conditions sont remplies, soit environ 60% ou 2 millisecondes de plus. Lorsque l'une ou les deux conditions sont fausses, alors une & s'exécute plus rapidement, mais il ne s'exécute qu'environ 0,30-0,50 millisecondes plus rapidement. Alors & s'exécutera plus rapidement que && dans la plupart des cas, mais la différence de performances reste négligeable.

6
Luke Melaia

Ce que vous recherchez est quelque chose comme ceci:

x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0  // integer bit-or

Intéressant, on aimerait presque regarder le code d'octet. Mais difficile à dire. Je souhaite que ce soit une question C.

3
Joop Eggen

La façon dont cela m'a été expliqué est que && renverra false si le premier contrôle d'une série est faux, tandis que & vérifie tous les éléments d'une série, quel que soit le nombre de faux. C'EST À DIRE.

si (x> 0 && x <= 10 && x

Courra plus vite que

si (x> 0 & x <= 10 & x

Si x est supérieur à 10, car les esperluettes simples continueront de vérifier le reste des conditions tandis que les esperluettes doubles se briseront après la première condition non vraie.

0
milkman

J'étais également curieux de la réponse, j'ai donc écrit le test (simple) suivant pour cela:

private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;

@Before
public void setUp() {
    this.random = new Random();
}

@After
public void tearDown() {
    random = null;
}

@Test
public void testSingleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of single operand: " + (end - start));
}

@Test
public void testDoubleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of double operand: " + (end - start));
}

Le résultat final étant que la comparaison avec && gagne toujours en termes de vitesse, soit environ 1,5/2 millisecondes plus rapidement que &.

EDIT: Comme l'a souligné @SvetlinZarev, je mesurais également le temps qu'il fallait à Random pour obtenir un entier. Il a été modifié pour utiliser un tableau prérempli de nombres aléatoires, ce qui a provoqué une fluctuation considérable de la durée du test de l'opérande unique; les différences entre plusieurs descentes étaient de 6 à 7 ms.

0
Oromë