In diesem Code:
if (value >= x && value <= y) {
Wann value >= x
und value <= y
wie wahrscheinlich wahr wie falsch ohne bestimmtes Muster, wäre die Verwendung des &
Operators schneller als die Verwendung&&
?
Insbesondere denke ich darüber nach, wie &&
träge der Ausdruck auf der rechten Seite bewertet wird (dh nur, wenn die LHS wahr ist), was eine Bedingung impliziert, während in Java &
in diesem Zusammenhang eine strikte Bewertung beider (boolescher) Unterausdrücke garantiert wird. Das Wertergebnis ist in beiden Fällen das gleiche.
Während ein >=
oder <=
Operator eine einfache Vergleichsanweisung verwendet, &&
muss diese eine Verzweigung beinhalten, und diese Verzweigung ist anfällig für Verzweigungsvorhersagefehler - gemäß dieser sehr berühmten Frage: Warum ist es schneller, ein sortiertes Array zu verarbeiten als ein unsortiertes Array?
Das Erzwingen, dass der Ausdruck keine faulen Komponenten enthält, ist sicherlich deterministischer und nicht anfällig für Vorhersagefehler. Richtig?
Anmerkungen:
- Offensichtlich wäre die Antwort auf meine Frage Nein, wenn der Code so aussehen würde :
if(value >= x && verySlowFunction())
. Ich konzentriere mich auf "ausreichend einfache" RHS-Ausdrücke. - Es gibt dort sowieso einen bedingten Zweig (die
if
Anweisung). Ich kann mir nicht ganz beweisen, dass das irrelevant ist und dass alternative Formulierungen bessere Beispiele sein könnten, wie zboolean b = value >= x && value <= y;
- Dies alles fällt in die Welt der schrecklichen Mikrooptimierungen. Ja, ich weiß :-) ... aber interessant?
Update Nur um zu erklären, warum ich interessiert bin: Ich habe auf die Systeme gestarrt, über die Martin Thompson in seinem Blog über mechanische Sympathie geschrieben hat , nachdem er gekommen war und einen Vortrag über Aeron gehalten hat. Eine der Schlüsselbotschaften ist, dass unsere Hardware all diese magischen Dinge enthält, und wir Softwareentwickler nutzen sie auf tragische Weise nicht aus. Keine Sorge, ich werde nicht meinen gesamten Code s / && / \ & / bearbeiten :-) ... aber es gibt eine Reihe von Fragen auf dieser Site zur Verbesserung der Zweigvorhersage durch Entfernen von Zweigen, und es ist aufgetreten Für mich sind die bedingten booleschen Operatoren das Kernstück der Testbedingungen.
Natürlich macht @StephenC den fantastischen Punkt deutlich, dass das Biegen Ihres Codes in seltsame Formen es JITs weniger leicht machen kann, allgemeine Optimierungen zu erkennen - wenn nicht jetzt, dann in der Zukunft. Und dass die oben erwähnte sehr berühmte Frage etwas Besonderes ist, weil sie die Komplexität der Vorhersage weit über die praktische Optimierung hinaus treibt.
Ich bin mir ziemlich bewusst, dass dies in den meisten (oder fast allen ) Situationen &&
das klarste, einfachste, schnellste und beste ist - obwohl ich den Leuten sehr dankbar bin, die Antworten gepostet haben, die dies demonstrieren! Ich bin wirklich interessiert zu sehen, ob es tatsächlich Fälle gibt, in denen die Antwort auf "Kann &
schneller sein?" könnte ja sein ...
Update 2 : (Adressierung des Hinweises, dass die Frage zu weit gefasst ist. Ich möchte keine wesentlichen Änderungen an dieser Frage vornehmen, da dies einige der folgenden Antworten gefährden könnte, die von außergewöhnlicher Qualität sind!) Vielleicht wird ein Beispiel in freier Wildbahn genannt zum; Dies ist aus der Guava LongMath- Klasse ( vielen Dank an @maaartinus für das Auffinden):
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
Sehen Sie das zuerst &
? Und wenn Sie den Link überprüfen, wird die nächste Methode aufgerufen lessThanBranchFree(...)
, die darauf hinweist, dass wir uns auf dem Gebiet der Vermeidung von Zweigen befinden - und Guave ist wirklich weit verbreitet: Jeder gespeicherte Zyklus führt dazu, dass der Meeresspiegel sichtbar sinkt. Stellen wir also die Frage so: Ist diese Verwendung von &
(wo &&
wäre normaler) eine echte Optimierung?
&
Operators eine wenig bekannte Besonderheit von Java im Hinblick auf eine Alternative zu&&
Java, und in Jahren der Java-Programmierung habe ich mich nie dafür entschieden, sie zu verwenden. Vielleicht war ich übermäßig abweisend!verySlowFunction()
Anmerkung); Hier geht es um die Vorhersage von Zweigen - oder sollte ich es noch etwas klarer machen? Vorschläge willkommen.&
über&&
hat einige echte Anwendungen .&
auch wenn Sie geschrieben haben,&&
wenn seine Heuristiken glauben, dass dies ein Gewinn wäre. Ich habe keine Ahnung, ob Javas Compiler dasselbe tut, aber es ist eine einfache Optimierung und es wäre ein bisschen überraschend, wenn sie nicht daran gedacht hätten.Antworten:
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 // 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
Die Methode
AndSC
(&&
) generiert erwartungsgemäß zwei bedingte Sprünge:value
undx
auf den Stapel, und springt auf L1 , wennvalue
niedriger ist. Sonst läuft es die nächsten Zeilen weiter.value
undy
auf den Stapel und springt auch zu L1, wennvalue
es größer ist. Sonst läuft es die nächsten Zeilen weiter.return true
Fall ist, wenn keiner der beiden Sprünge gemacht wurde.return false
.Die
AndNonSC
(&
) -Methode generiert jedoch drei bedingte Sprünge!value
undx
auf den Stapel und springt zu L1, wennvalue
es niedriger ist. Da jetzt das Ergebnis gespeichert werden muss, um es mit dem anderen Teil des UND zu vergleichen, sodass entweder "Speicherntrue
" oder "Speichernfalse
" ausgeführt werden muss, kann es nicht beide mit derselben Anweisung ausführen .value
undy
auf den Stapel und springt zu L1, wennvalue
es größer ist. Noch einmal muss es gespeichert werdentrue
oderfalse
und das sind zwei verschiedene Zeilen, abhängig vom Vergleichsergebnis.(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
AndSC
, wobei jeder BytecodeIF_ICMP*
in zwei Assembler-Sprunganweisungen übersetzt wird, was insgesamt 4 bedingten Sprüngen entspricht.AndNonSC
generiert der Compiler für die Methode einen einfacheren Code, bei dem jeder BytecodeIF_ICMP*
in nur einen Assembler-Sprungbefehl übersetzt wird, wobei die ursprüngliche Anzahl von 3 bedingten Sprüngen beibehalten wird.AndSC
ist kürzer mit nur 2 bedingten Sprüngen (ohne Berücksichtigung der nicht bedingtenjmp
am Ende). Tatsächlich sind es je nach Ergebnis nur zwei CMP, zwei JL / E und ein XOR / MOV.AndNonSC
ist jetzt länger als derAndSC
! 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
&
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 .&&
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
MethodeHier 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:
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:
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 .
quelle
javac
möglicherweise einen anderen Code als das offizielle Oracle oder das OpenJDK aus ... Und natürlich würde sich der Maschinencode in einem X86-Computer wahrscheinlich von einem PowerPC AIX-System oder den in vielen Smartphones verwendeten Snapdragon-CPUs unterscheiden - jede Plattform wird es tun haben ihre eigenen Compiler und Optimierungen. Aber in einem einfachen Fall wie diesem bezweifle ich, dass die Unterschiede von einer CPU zur anderen einen größeren Unterschied machen als 2 gegen 3 Bytecode-bedingte Sprünge.Für diese Art von Fragen sollten Sie ein Mikrobenchmark ausführen. Ich habe JMH für diesen Test verwendet.
Die Benchmarks werden als implementiert
// boolean logical AND bh.consume(value >= x & y <= value);
und
// conditional AND bh.consume(value >= x && y <= value);
und
// bitwise OR, as suggested by Joop Eggen bh.consume(((value - x) | (y - value)) >= 0)
Mit Werten für
value, x and y
gemäß dem Benchmark-Namen.Das Ergebnis (fünf Aufwärm- und zehn Messungsiterationen) für das Durchsatz-Benchmarking lautet:
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
Das Ergebnis ist für die Bewertung selbst nicht so unterschiedlich. Solange auf diesem Code keine Auswirkungen auf die Leistung festgestellt werden, würde ich nicht versuchen, ihn zu optimieren. Abhängig von der Stelle im Code entscheidet sich der Hotspot-Compiler möglicherweise für eine Optimierung. Was wahrscheinlich nicht durch die oben genannten Benchmarks abgedeckt ist.
Einige Referenzen:
boolesches logisches UND - der Ergebniswert ist,
true
wenn beide Operandenwerte sindtrue
; Andernfalls ist das Ergebnisfalse
bedingt UND - ist ähnlich
&
, wertet jedoch seinen rechten Operanden nur aus, wenn der Wert seines linken Operandentrue
bitweise ODER ist - der Ergebniswert ist das bitweise inklusive ODER der Operandenwerte
quelle
&
. Wenn Sie also die ursprüngliche Frage beantworten - nein, generiert JVM immer noch einen bedingten Zweig für&
.&
wäre es in einigen Fällen besser! Kommentare sind willkommen :-)methodWithSideEffects()
wird nicht übersprungen, sonst wäre es eine Spezifikationsverletzung. In diesem Fall könnte jedoch eine Methode ohne Nebenwirkungen optimiert werden.Ich werde dies aus einem anderen Blickwinkel betrachten.
Betrachten Sie diese beiden Codefragmente.
if (value >= x && value <= y) {
und
if (value >= x & value <= y) {
Wenn wir davon ausgehen , dass
value
,x
,y
haben eine primitive Art, dann diese beiden (Teil-) Aussagen wird das gleiche Ergebnis für alle möglichen Eingabewerte geben. (Wenn Wrapper-Typen beteiligt sind, sind sie nicht genau gleichwertig, da ein impliziternull
Test dafüry
möglicherweise in der&
Version und nicht in der Version fehlschlägt&&
.)Wenn der JIT-Compiler gute Arbeit leistet, kann sein Optimierer daraus schließen, dass diese beiden Anweisungen dasselbe tun:
Wenn einer vorhersehbar schneller als der andere ist, sollte er in der Lage sein, die schnellere Version ... im kompilierten JIT-Code zu verwenden .
Wenn nicht, spielt es keine Rolle, welche Version auf Quellcodeebene verwendet wird.
Da der JIT-Compiler vor dem Kompilieren Pfadstatistiken sammelt, kann er möglicherweise mehr Informationen über die Ausführungseigenschaften des Programmierers (!) Enthalten.
Wenn der JIT-Compiler der aktuellen Generation (auf einer bestimmten Plattform) nicht gut genug optimiert, um dies zu handhaben, könnte die nächste Generation dies gut tun ... abhängig davon, ob empirische Beweise darauf hinweisen, dass dies ein lohnendes Optimierungsmuster ist oder nicht .
Wenn Sie Ihren Java-Code so schreiben, dass dies optimiert wird, besteht die Möglichkeit, dass Sie durch Auswahl der "dunkeleren" Version des Codes die Optimierungsfähigkeit des aktuellen oder zukünftigen JIT-Compilers beeinträchtigen.
Kurz gesagt, ich denke nicht, dass Sie diese Art der Mikrooptimierung auf Quellcodeebene durchführen sollten. Und wenn Sie dieses Argument 1 akzeptieren und es zu seiner logischen Schlussfolgerung führen, ist die Frage, welche Version schneller ist, ... strittig 2 .
1 - Ich behaupte nicht, dass dies ein Beweis ist.
2 - Es sei denn, Sie gehören zu der winzigen Community von Leuten, die tatsächlich Java JIT-Compiler schreiben ...
Die "sehr berühmte Frage" ist in zweierlei Hinsicht interessant:
Einerseits ist dies ein Beispiel, bei dem die Art der Optimierung, die erforderlich ist, um einen Unterschied zu bewirken, weit über die Fähigkeiten eines JIT-Compilers hinausgeht.
Andererseits wäre es nicht unbedingt das Richtige, das Array zu sortieren ... nur weil ein sortiertes Array schneller verarbeitet werden kann. Die Kosten für das Sortieren des Arrays könnten (viel) höher sein als die Einsparungen.
quelle
x
undy
entweder Konstanten oder vorhersagbar Wert ist, wird den optimierte Code eher aussehen ,value-x ≤ͧ y-x
wo≤ͧ
einunsigned long
Vergleich undy-x
eine Konstante, obwohl selbst wennx
undy
nicht vorhersehbar sind, dass einzelne Vergleichsvariante verwendet werden könnte, wenn zwei Zweige in Betracht gezogen werden teurer als eine mit Spannung durchgeführter Vergleich (ein numerischer Vergleich entspricht der Minusoperation). Also darüber nachzudenken&
und&&
macht in der Tat keinen Sinn.Wenn Sie eine
&
oder&&
mehrere Bedingungen verwenden, muss diese ausgewertet werden, sodass es unwahrscheinlich ist, dass Verarbeitungszeit eingespart wird. Dies kann sogar dazu führen, dass Sie beide Ausdrücke auswerten, wenn Sie nur einen auswerten müssen.Verwenden von
&
over&&
zum Speichern einer Nanosekunde, wenn dies in einigen sehr seltenen Situationen sinnlos ist, haben Sie bereits mehr Zeit damit verschwendet, über den Unterschied nachzudenken, als Sie mit&
over gespeichert hätten&&
.Bearbeiten
Ich wurde neugierig und beschloss, ein paar Benchmarks zu machen.
Ich habe diese Klasse gemacht:
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){ } } }
und führte einige Profiling-Tests mit NetBeans durch. Ich habe keine Druckanweisungen verwendet, um Verarbeitungszeit zu sparen. Ich weiß nur, dass beide auswerten
true
.Erster Test:
Zweiter Test:
Dritter Test:
Wie Sie den Profiling-Tests entnehmen können,
&
dauert die Ausführung nur eines 2-3-mal länger als die Verwendung von zwei&&
. Dies ist etwas seltsam, da ich von nur einem eine bessere Leistung erwartet habe&
.Ich bin mir nicht 100% sicher warum. In beiden Fällen müssen beide Ausdrücke ausgewertet werden, da beide wahr sind. Ich vermute, dass die JVM hinter den Kulissen einige spezielle Optimierungen vornimmt, um sie zu beschleunigen.
Moral der Geschichte: Konvention ist gut und vorzeitige Optimierung ist schlecht.
Bearbeiten 2
Ich habe den Benchmark-Code unter Berücksichtigung der Kommentare von @ SvetlinZarev und einiger anderer Verbesserungen überarbeitet. Hier ist der modifizierte Benchmark-Code:
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"); } }
Und hier sind die Leistungstests:
Test 1:
Test 2:
Test 3:
Dies berücksichtigt auch unterschiedliche Werte und unterschiedliche Bedingungen.
&
Wenn beide Bedingungen erfüllt sind, dauert die Verwendung von one länger, etwa 60% oder 2 Millisekunden länger. Wenn eine oder beide Bedingungen falsch sind,&
läuft eine schneller, aber nur etwa 0,30 bis 0,50 Millisekunden schneller. Läuft also&
schneller als&&
in den meisten Fällen, aber der Leistungsunterschied ist immer noch vernachlässigbar.quelle
Was Sie suchen, ist ungefähr so:
x <= value & value <= y value - x >= 0 & y - value >= 0 ((value - x) | (y - value)) >= 0 // integer bit-or
Interessanterweise möchte man sich fast den Bytecode ansehen. Aber schwer zu sagen. Ich wünschte, dies wäre eine C-Frage.
quelle
Ich war auch neugierig auf die Antwort und habe den folgenden (einfachen) Test dafür geschrieben:
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)); }
Das Endergebnis ist, dass der Vergleich mit && immer in Bezug auf die Geschwindigkeit gewinnt und etwa 1,5 / 2 Millisekunden schneller als & ist.
EDIT: Wie @SvetlinZarev betonte, habe ich auch die Zeit gemessen, die Random brauchte, um eine ganze Zahl zu erhalten. Es wurde geändert, um ein vorgefülltes Array von Zufallszahlen zu verwenden, wodurch die Dauer des Einzeloperandentests stark schwankte. Die Unterschiede zwischen mehreren Läufen betrugen bis zu 6-7 ms.
quelle
generated >= x
), was bedeutet, dass der Prädiktor normalerweise die Dinge richtig macht (wenn es so funktioniert, wie ich denke, dass es funktioniert). Ich werde versuchen, mit diesen 'x'- und' y'-Werten herumzuspielen - ich denkex=40000
undy=60000
werde interessant sein (50% Erfolg bei jedem Test).random.nextInt()
da es viel länger dauert als das einfache && oder &. Ihre Tests sind fehlerhaftDie Art und Weise, wie mir dies erklärt wurde, ist, dass && false zurückgibt, wenn die erste Prüfung in einer Reihe falsch ist, während & alle Elemente in einer Reihe prüft, unabhängig davon, wie viele falsch sind. IE
if (x> 0 && x <= 10 && x
Läuft schneller als
if (x> 0 & x <= 10 & x
Wenn x größer als 10 ist, überprüfen einzelne kaufmännische Und-Zeichen weiterhin den Rest der Bedingungen, während doppelte kaufmännische Und-Zeichen nach der ersten nicht wahren Bedingung brechen.
quelle