Ok, Sie möchten wissen, wie es sich auf der unteren Ebene verhält ... Dann schauen wir uns den Bytecode an!
BEARBEITEN: Am Ende wurde der generierte Assemblycode für AMD64 hinzugefügt. Schauen Sie sich einige interessante Hinweise an.
EDIT 2 (re: OPs "Update 2"): Asm-Code für Guavas isPowerOfTwo
Methode hinzugefügt .
Java-Quelle
Ich habe diese beiden schnellen Methoden geschrieben:
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;
}
Wie Sie sehen können, sind sie bis auf den Typ des AND-Operators genau gleich.
Java-Bytecode
Und das ist der generierte Bytecode:
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
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
Die Methode AndSC
( &&
) generiert erwartungsgemäß zwei bedingte Sprünge:
- Es lädt
value
und x
auf den Stapel, und springt auf L1 , wenn value
niedriger ist. Sonst läuft es die nächsten Zeilen weiter.
- Es lädt
value
und y
auf den Stapel und springt auch zu L1, wenn value
es größer ist. Sonst läuft es die nächsten Zeilen weiter.
- Was zufällig der
return true
Fall ist, wenn keiner der beiden Sprünge gemacht wurde.
- Und dann haben wir die als L1 markierten Linien, die a sind
return false
.
Die AndNonSC
( &
) -Methode generiert jedoch drei bedingte Sprünge!
- Es lädt
value
und x
auf den Stapel und springt zu L1, wenn value
es niedriger ist. Da jetzt das Ergebnis gespeichert werden muss, um es mit dem anderen Teil des UND zu vergleichen, sodass entweder "Speichern true
" oder "Speichern false
" ausgeführt werden muss, kann es nicht beide mit derselben Anweisung ausführen .
- Es lädt
value
und y
auf den Stapel und springt zu L1, wenn value
es größer ist. Noch einmal muss es gespeichert werden true
oder false
und das sind zwei verschiedene Zeilen, abhängig vom Vergleichsergebnis.
- Nachdem beide Vergleiche durchgeführt wurden, führt der Code tatsächlich die UND-Operation aus - und wenn beide wahr sind, springt er (zum dritten Mal), um wahr zurückzugeben. Andernfalls wird die Ausführung in der nächsten Zeile fortgesetzt, um false zurückzugeben.
(Vorläufige) Schlussfolgerung
Obwohl ich mit Java-Bytecode nicht so viel Erfahrung habe und möglicherweise etwas übersehen habe, scheint es mir, dass &
es tatsächlich schlechter abschneidet als &&
in jedem Fall: Es generiert mehr Anweisungen zum Ausführen, einschließlich mehr bedingter Sprünge zum Vorhersagen und möglicherweise zum Fehlschlagen .
Ein Umschreiben des Codes, um Vergleiche mit arithmetischen Operationen zu ersetzen, wie von jemand anderem vorgeschlagen, könnte eine Möglichkeit sein, &
eine bessere Option zu finden, jedoch auf Kosten einer wesentlich geringeren Klarheit des Codes.
IMHO lohnt sich der Aufwand für 99% der Szenarien nicht (es kann sich jedoch für die 1% -Schleifen lohnen, die extrem optimiert werden müssen).
BEARBEITEN: AMD64-Baugruppe
Wie in den Kommentaren erwähnt, kann derselbe Java-Bytecode in verschiedenen Systemen zu unterschiedlichem Maschinencode führen. Während der Java-Bytecode uns möglicherweise einen Hinweis darauf gibt, welche AND-Version eine bessere Leistung erbringt, ist der vom Compiler generierte tatsächliche ASM der einzige Weg um es wirklich herauszufinden.
Ich habe die AMD64 ASM-Anweisungen für beide Methoden gedruckt. Unten sind die relevanten Linien (gestrippte Einstiegspunkte usw.) aufgeführt.
HINWEIS: Alle mit Java 1.8.0_91 kompilierten Methoden, sofern nicht anders angegeben.
Methode AndSC
mit Standardoptionen
# {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
Methode AndSC
mit -XX:PrintAssemblyOptions=intel
Option
# {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
Methode AndNonSC
mit Standardoptionen
# {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
Methode AndNonSC
mit -XX:PrintAssemblyOptions=intel
Option
# {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
- Erstens unterscheidet sich der generierte ASM-Code je nachdem, ob wir die Standard-AT & T-Syntax oder die Intel-Syntax wählen.
- Mit AT & T-Syntax:
- Der ASM-Code ist für die Methode tatsächlich länger
AndSC
, wobei jeder Bytecode IF_ICMP*
in zwei Assembler-Sprunganweisungen übersetzt wird, was insgesamt 4 bedingten Sprüngen entspricht.
- Währenddessen
AndNonSC
generiert der Compiler für die Methode einen einfacheren Code, bei dem jeder Bytecode IF_ICMP*
in nur einen Assembler-Sprungbefehl übersetzt wird, wobei die ursprüngliche Anzahl von 3 bedingten Sprüngen beibehalten wird.
- Mit Intel-Syntax:
- Der ASM-Code für
AndSC
ist kürzer mit nur 2 bedingten Sprüngen (ohne Berücksichtigung der nicht bedingten jmp
am Ende). Tatsächlich sind es je nach Ergebnis nur zwei CMP, zwei JL / E und ein XOR / MOV.
- Der ASM-Code für
AndNonSC
ist jetzt länger als der AndSC
! Jedoch , es muss nur 1 bedingten Sprung (für den ersten Vergleich), die Register verwendet , um direkt das erste Ergebnis mit dem zweiten zu vergleichen, ohne mehr springt.
Schlussfolgerung nach ASM-Code-Analyse
- Auf AMD64-Maschinensprachenebene
&
scheint der Bediener ASM-Code mit weniger bedingten Sprüngen zu generieren, was für hohe Vorhersagefehlerraten ( value
z. B. zufällige s) besser sein könnte .
- Auf der anderen Seite
&&
scheint der Bediener ASM-Code mit weniger Anweisungen zu generieren (mit der -XX:PrintAssemblyOptions=intel
Option sowieso), was für wirklich lange Schleifen mit vorhersagefreundlichen Eingaben besser sein könnte , bei denen die geringere Anzahl von CPU-Zyklen für jeden Vergleich einen Unterschied machen kann auf Dauer.
Wie ich in einigen Kommentaren festgestellt habe, wird dies zwischen den Systemen sehr unterschiedlich sein. Wenn wir also über die Optimierung der Verzweigungsvorhersage sprechen, wäre die einzige wirkliche Antwort: Es hängt von Ihrer JVM-Implementierung, Ihrem Compiler, Ihrer CPU und ab Ihre Eingabedaten .
Nachtrag: Guavas isPowerOfTwo
Methode
Hier haben die Entwickler von Guava eine übersichtliche Methode gefunden, um zu berechnen, ob eine bestimmte Zahl eine Potenz von 2 ist:
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
Zitat OP:
Ist diese Verwendung von &
(wo &&
wäre normaler) eine echte Optimierung?
Um herauszufinden, ob dies der Fall ist, habe ich meiner Testklasse zwei ähnliche Methoden hinzugefügt:
public boolean isPowerOfTwoAND(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
public boolean isPowerOfTwoANDAND(long x) {
return x > 0 && (x & (x - 1)) == 0;
}
Intels ASM-Code für Guavas Version
# {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
Intels ASM-Code für die &&
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
In diesem speziellen Beispiel generiert der JIT-Compiler für die Version weit weniger Assembler-Code &&
als für die Guava- &
Version (und nach den gestrigen Ergebnissen war ich ehrlich überrascht).
Im Vergleich zu Guava bedeutet die &&
Version 25% weniger Bytecode für die Kompilierung von JIT, 50% weniger Montageanweisungen und nur zwei bedingte Sprünge (die &
Version enthält vier davon).
Alles deutet also darauf hin, dass Guavas &
Methode weniger effizient ist als die "natürlichere" &&
Version.
... Oder ist es?
Wie bereits erwähnt, führe ich die obigen Beispiele mit Java 8 aus:
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)
Aber was ist, wenn ich zu Java 7 wechsle ?
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
.....
Überraschung! Der &
vom JIT-Compiler in Java 7 für die Methode generierte Assembler-Code hat jetzt nur noch einen bedingten Sprung und ist viel kürzer! Während die &&
Methode (Sie müssen mir in dieser Sache vertrauen, ich möchte das Ende nicht überladen!) Mit ihren zwei bedingten Sprüngen und ein paar weniger Anweisungen ungefähr gleich bleibt.
Es sieht so aus, als hätten Guavas Ingenieure doch gewusst, was sie taten! (Wenn sie versuchten, die Ausführungszeit von Java 7 zu optimieren, ist das ;-)
Zurück zur letzten Frage von OP:
Ist diese Verwendung von &
(wo &&
wäre normaler) eine echte Optimierung?
Und meiner Meinung nach ist die Antwort auch für dieses (sehr!) Spezifische Szenario dieselbe : Sie hängt von Ihrer JVM-Implementierung, Ihrem Compiler, Ihrer CPU und Ihren Eingabedaten ab .