Kann & in Java schneller sein als &&?

72

In diesem Code:

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

Wann value >= xund value <= ywie 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 ifAnweisung). 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?

SusanW
quelle
16
Wenn es einen Unterschied gibt, sind es Nanosekunden. Dies riecht nach vorzeitiger Optimierung. Warum ist es wichtig? Wenn Sie es wirklich wissen wollen, schauen Sie sich einfach den kompilierten Bytecode an.
Jim Garrison
6
@JimGarrison Dies ist wichtig, da Tests wie diese häufig in Komparatoren (dh Sortieren) und Filtern verwendet werden, sodass Millionen von Ausführungen in einer engen Schleife häufig vorkommen und ns dann zu ms werden. Außerdem ist die strikte Bewertung des &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!
SusanW
2
@pavlos - Ich dachte, ich hätte das in der Frage sehr deutlich gemacht (siehe verySlowFunction()Anmerkung); Hier geht es um die Vorhersage von Zweigen - oder sollte ich es noch etwas klarer machen? Vorschläge willkommen.
SusanW
3
FWIW, sieht es aus wie &über &&hat einige echte Anwendungen .
Maaartinus
5
Der C # -Compiler generiert Code so, als hätten Sie geschrieben, &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.
Eric Lippert

Antworten:

75

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 isPowerOfTwoMethode 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:

  1. Es lädt valueund xauf den Stapel, und springt auf L1 , wenn valueniedriger ist. Sonst läuft es die nächsten Zeilen weiter.
  2. Es lädt valueund yauf den Stapel und springt auch zu L1, wenn valuees größer ist. Sonst läuft es die nächsten Zeilen weiter.
  3. Was zufällig der return trueFall ist, wenn keiner der beiden Sprünge gemacht wurde.
  4. Und dann haben wir die als L1 markierten Linien, die a sind return false.

Die AndNonSC( &) -Methode generiert jedoch drei bedingte Sprünge!

  1. Es lädt valueund xauf den Stapel und springt zu L1, wenn valuees 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 .
  2. Es lädt valueund yauf den Stapel und springt zu L1, wenn valuees größer ist. Noch einmal muss es gespeichert werden trueoder falseund das sind zwei verschiedene Zeilen, abhängig vom Vergleichsergebnis.
  3. 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 AndSCmit 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 AndSCmit -XX:PrintAssemblyOptions=intelOption

  # {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 AndNonSCmit 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 AndNonSCmit -XX:PrintAssemblyOptions=intelOption

  # {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ängerAndSC , wobei jeder Bytecode IF_ICMP*in zwei Assembler-Sprunganweisungen übersetzt wird, was insgesamt 4 bedingten Sprüngen entspricht.
    • Währenddessen AndNonSCgeneriert 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 AndSCist kürzer mit nur 2 bedingten Sprüngen (ohne Berücksichtigung der nicht bedingten jmpam Ende). Tatsächlich sind es je nach Ergebnis nur zwei CMP, zwei JL / E und ein XOR / MOV.
    • Der ASM-Code für AndNonSCist 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 ( valuez. 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=intelOption 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 isPowerOfTwoMethode

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 .

walen
quelle
8
Nun, Java-Bytecode kommt ASM am nächsten, bevor Sie sich mit den Besonderheiten jedes Betriebssystems und jeder CPU befassen. Sicher, die IBM gibt javacmö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.
walen
9
Es ist zwar "am nächsten an ASM", aber nicht nah genug, um logische Schlussfolgerungen ziehen zu können. Einfach ausgedrückt, nachdem der Code JIT-kompiliert wurde, führt die JVM keine Bytecodes aus.
Stephen C
1
@walen Du hast es geklärt. Sie sagten ursprünglich Sprung statt bedingten Sprung (was wirklich ein Zweig ist). Es gibt nur einen Ort, an dem man springen kann, also ist nichts vorherzusagen. Daher konnte es keine falsche Vorhersage geben.
Riley
2
@Riley ja, das sind Sie, aber ich kann mich darauf beziehen, also kein Problem :) Gestatten Sie mir, das offizielle Intel Developer Manual für Intel ® 64- und IA-32-Architekturen zu zitieren : " 5.1.7 Anweisungen zur Steuerübertragung Die Anweisungen zur Steuerübertragung enthalten einen Sprung und einen bedingten Sprung , Schleife und Aufruf und Rückgabe von Operationen zur Steuerung des Programmflusses. "
walen
2
Ich denke, das ist eine fantastische Antwort. Es ist möglich, dass Java8 subtil ist, wodurch weitere Optimierungen auf der Basis von HotSpot-Magie oder Ähnlichem vorgenommen werden können. In diesem Fall könnte eine neue Frage geboren werden ... in der Zwischenzeit eine nette! Vielen Dank!
SusanW
23

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 ygemäß 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.08617.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.2407.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.84715.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.87711.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.74315.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.52416.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.19019.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.09415.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.9135.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, truewenn beide Operandenwerte sind true; Andernfalls ist das Ergebnis false
bedingt UND - ist ähnlich &, wertet jedoch seinen rechten Operanden nur aus, wenn der Wert seines linken Operanden true
bitweise ODER ist - der Ergebniswert ist das bitweise inklusive ODER der Operandenwerte

Suboptimal
quelle
4
Das ist der bisher beste Benchmark, aber er ist auch fehlerhaft :) Das Blackhole benötigt viel mehr Zeit als das && oder & so messen Sie im Grunde die Blackhole-Leistung :) Versuchen Sie es mit etwas wie Konsumieren (a & b & c 7 d & f) & g .... & z);
Svetlin Zarev
1
@SusanW BTW, es war JMH Fehler , die entdecken geholfen , dass HotSpot ist die Bewertung der Verknüpfung &. Wenn Sie also die ursprüngliche Frage beantworten - nein, generiert JVM immer noch einen bedingten Zweig für &.
Apangin
1
@SusanW @SubOptimal Ich habe meine Antwort so bearbeitet, dass sie den tatsächlich von JIT generierten ASM-Code enthält. Und es sieht so aus, als & wäre es in einigen Fällen besser! Kommentare sind willkommen :-)
walen
1
@SusanW Nein, methodWithSideEffects()wird nicht übersprungen, sonst wäre es eine Spezifikationsverletzung. In diesem Fall könnte jedoch eine Methode ohne Nebenwirkungen optimiert werden.
Apangin
1
Es gibt bereits viel Verwirrung um die Bedeutung der logischen Operatoren ohne Verknüpfung. Können Sie diesen Beitrag bitte so ändern, dass er nicht als bitweise bezeichnet wird? Ihr Test enthält keine bitweisen Berechnungen.
JimmyJames
13

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, yhaben 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 impliziter nullTest dafür ymö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.

Stephen C.
quelle
3
Ihr Argument, zukünftige Optimierungen zu verhindern, ist sehr gut gemacht ! - Das absichtliche Einfügen von '&' wäre gleichbedeutend mit "Versäumnis, Absichten klar auszudrücken, um das System
auszutricksen
Welches schneller ist, ist datenabhängig. Dies kann die GEG nicht wissen. Oder können JVM JITs so etwas profilieren? In diesem Fall wäre dies durchaus machbar.
usr
Ja. Ein JIT kann das tun. Und HotSpot JIT-Compiler tun dies in der Phase, bevor die Bytecodes interpretiert werden ... vor der Kompilierung.
Stephen C
Wenn xund yentweder Konstanten oder vorhersagbar Wert ist, wird den optimierte Code eher aussehen , value-x ≤ͧ y-xwo ≤ͧein unsigned longVergleich und y-xeine Konstante, obwohl selbst wenn xund ynicht 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.
Holger
1
Zukünftige Optimierungen - lieben diesen Aspekt. Überlegen Sie, wie sich "a + b + c" in die Verwendung von StringBuffers verwandelt hat, auch wenn sie vielleicht nicht wirklich wichtig waren. Als dann StringBuilders herauskamen, haben die Leute diese großen, fadensicheren StringBuffers, bei denen ein solcher Overhead unnötig war. Jetzt stellt "a + b + c" beim Kompilieren auf StringBuilder ein, aber explizite StringBuffer existieren offensichtlich immer noch aufgrund eifriger Überoptimierung.
CorsiKa
6

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:

Der erste Profiling-Test

Zweiter Test:

Der zweite Profiling-Test

Dritter Test:

Der dritte Profiling-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:

Geben Sie hier die Bildbeschreibung ein

Test 2:

Geben Sie hier die Bildbeschreibung ein

Test 3:

Geben Sie hier die Bildbeschreibung ein

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.

Luke Melaia
quelle
5
Ihr Mikro-Benchmark ist völlig fehlerhaft. Die JIT optimiert die leeren for-Schleifen, ganz zu schweigen davon, dass eine einzelne Ausführung der Methode wie in Ihrem Code niemals zu aussagekräftigen Ergebnissen führen kann.
Svetlin Zarev
1
Vielen Dank für den Hinweis, ich werde die Tests in diesem Sinne wiederholen.
Luke Melaia
4
Die einzig richtige Art der Mikrobenchmarkierung ist die Verwendung eines Tools wie JMH.
Svetlin Zarev
Wenn Sie nicht auf einem wirklich alten Computer ausgeführt werden, werden Ihre Schleifen nicht oft genug ausgeführt, um aussagekräftige Ergebnisse zu erzielen. Auch die Reihenfolge, in der Sie Dinge anrufen, kann einen großen Unterschied machen. Wenn Sie weiterhin an einen StringBuilder anhängen, muss schließlich viel Speicher zugewiesen werden, was sehr lange dauern wird.
JimmyJames
'BothFalse' ist ungültig. Diese Verfahren mit 100 Test der gleichen wie 60. Sie können nicht beide unter dem Bereich und über dem Bereich zugleich sein, so BothFalse unerreichbaren ist ..
Sinc
3

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.

Joop Eggen
quelle
0

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.

Oromë
quelle
Ok, interessant: Ich kann sehen, dass die erste Bedingung meistens erfolgreich sein wird ( 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 denke x=40000und y=60000werde interessant sein (50% Erfolg bei jedem Test).
SusanW
Mit diesen Werten schlägt && immer noch &. Diesmal schien der durchschnittliche Unterschied zwischen den beiden ebenfalls höher zu sein und fiel nie unter 2 ms und gelegentlich sogar über 3 ms.
Oromë
5
Sie messen das, random.nextInt()da es viel länger dauert als das einfache && oder &. Ihre Tests sind fehlerhaft
Svetlin Zarev
1
@SvetlinZarev Guter Punkt zum zufälligen Kommentar; Ich habe es so geändert, dass ein Array mit zufälligen Ganzzahlen verwendet wird. Das gleiche Endergebnis ist, dass && schneller als & ist.
Oromë
2
@ Oromë Ihnen fehlt noch Aufwärmen :)
Svetlin Zarev
0

Die 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.

Milchmann
quelle
Entschuldigung, das geht am eigentlichen Punkt der Frage vorbei! Schauen Sie sich die erste "Notiz" in der Frage an - ich war ziemlich explizit darüber. Wenn erhebliche Zeit gespart werden kann, indem die nachfolgenden Bedingungen nicht ausgeführt werden, wissen wir das natürlich alle. Dazu gehört jedoch eine Verzweigung , und moderne Prozessoranweisungs-Pipelines raten manchmal über die Richtung, in die eine Verzweigung führt, was sich als a) falsch und b) ziemlich teuer herausstellt. Bitte lesen Sie die Top-Antwort auf die (sehr berühmte) Frage, mit der ich verlinkt habe, und entscheiden Sie dann, ob Sie diese Antwort behalten möchten.
SusanW