it-swarm.com.de

Warum ist (a * b! = 0) in Java schneller als (a! = 0 && b! = 0)?

Ich schreibe einen Code in Java, wobei der Programmfluss irgendwann davon abhängt, ob zwei int - Variablen "a" und "b" ungleich Null sind (Anmerkung : a und b sind niemals negativ und niemals im ganzzahligen Überlaufbereich).

Ich kann es mit bewerten

if (a != 0 && b != 0) { /* Some code */ }

Oder alternativ

if (a*b != 0) { /* Some code */ }

Da ich davon ausgegangen bin, dass dieser Code millionenfach pro Lauf ausgeführt wird, habe ich mich gefragt, welcher Code schneller sein würde. Ich habe das Experiment durchgeführt, indem ich sie mit einem großen, zufällig generierten Array verglichen habe, und ich war auch gespannt, wie sich die Sparsamkeit des Arrays (Bruchteil der Daten = 0) auf die Ergebnisse auswirken würde:

long time;
final int len = 50000000;
int arbitrary = 0;
int[][] nums = new int[2][len];

for (double fraction = 0 ; fraction <= 0.9 ; fraction += 0.0078125) {
    for(int i = 0 ; i < 2 ; i++) {
        for(int j = 0 ; j < len ; j++) {
            double random = Math.random();

            if(random < fraction) nums[i][j] = 0;
            else nums[i][j] = (int) (random*15 + 1);
        }
    }

    time = System.currentTimeMillis();

    for(int i = 0 ; i < len ; i++) {
        if( /*insert nums[0][i]*nums[1][i]!=0 or nums[0][i]!=0 && nums[1][i]!=0*/ ) arbitrary++;
    }
    System.out.println(System.currentTimeMillis() - time);
}

Und die Ergebnisse zeigen, dass a*b != 0 Schneller ist als a!=0 && b!=0, Wenn Sie erwarten, dass "a" oder "b" mehr als ~ 3% der Zeit gleich 0 sind:

Graphical graph of the results of a AND b non-zero

Ich bin gespannt warum. Könnte jemand Licht ins Dunkel bringen? Ist es der Compiler oder ist es auf der Hardware-Ebene?

Edit: Aus Neugier ... Nachdem ich nun etwas über die Verzweigungsvorhersage gelernt hatte, fragte ich mich, was der analoge Vergleich ist würde für a [~ # ~] oder [~ # ~] b ungleich Null zeigen:

Graph of a or b non-zero

Wir sehen den gleichen Effekt der Verzweigungsvorhersage wie erwartet, interessanterweise ist der Graph entlang der X-Achse etwas gespiegelt.

Aktualisieren

1- Ich habe !(a==0 || b==0) zur Analyse hinzugefügt, um zu sehen, was passiert.

2- Ich habe auch a != 0 || b != 0, (a+b) != 0 Und (a|b) != 0 Aus Neugier aufgenommen, nachdem ich etwas über die Verzweigungsvorhersage gelernt hatte. Sie sind jedoch nicht logisch äquivalent zu den anderen Ausdrücken, da nur a [~ # ~] oder [~ # ~] b ungleich Null sein muss, um true zurückzugeben, sodass sie nicht gemeint sind verglichen werden für die Verarbeitungseffizienz.

3- Ich habe auch den eigentlichen Benchmark hinzugefügt, den ich für die Analyse verwendet habe. Dabei handelt es sich nur um die Iteration einer beliebigen int-Variablen.

4- Einige Leute schlugen vor, a != 0 & b != 0 Anstelle von a != 0 && b != 0 Einzufügen, mit der Vorhersage, dass es sich näher an a*b != 0 Verhalten würde, weil wir den Verzweigungsvorhersageeffekt entfernen würden. Ich wusste nicht, dass & Mit booleschen Variablen verwendet werden kann, ich dachte, es wurde nur für Binäroperationen mit ganzen Zahlen verwendet.

Hinweis: In dem Kontext, in dem ich all dies in Betracht gezogen habe, ist der int-Überlauf kein Problem, aber dies ist definitiv ein wichtiger Aspekt in allgemeinen Kontexten.

CPU: Intel Core i7-3610QM bei 2,3 GHz

Java-Version: 1.8.0_45
Java (TM) SE-Laufzeitumgebung (Build 1.8.0_45-b14)
Java HotSpot (TM) 64-Bit-Server VM (Build 25.45-b02, gemischter Modus)

388
Maljam

Ich ignoriere das Problem, dass Ihr Benchmarking könnte fehlerhaft ist, und nehme das Ergebnis zum Nennwert.

Ist es der Compiler oder ist es auf der Hardware-Ebene?

Letzteres denke ich:

  if (a != 0 && b != 0)

kompiliert 2 Speicherladevorgänge und zwei bedingte Verzweigungen

  if (a * b != 0)

kompiliert 2 Speicherladevorgänge, einen Multiplikations- und einen bedingten Zweig.

Die Multiplikation ist wahrscheinlich schneller als die zweite bedingte Verzweigung, wenn die Verzweigungsvorhersage auf Hardwareebene unwirksam ist. Wenn Sie das Verhältnis erhöhen ... wird die Verzweigungsvorhersage immer weniger wirksam.

Der Grund dafür, dass bedingte Verzweigungen langsamer sind, besteht darin, dass die Anweisungsausführungspipeline blockiert. Bei der Verzweigungsvorhersage geht es darum, den Strömungsabriss zu vermeiden, indem vorausgesagt wird, in welche Richtung sich die Verzweigung bewegen wird, und basierend darauf spekulativ der nächste Befehl ausgewählt wird. Wenn die Vorhersage fehlschlägt, gibt es eine Verzögerung, während der Befehl für die andere Richtung geladen wird.

(Hinweis: Die obige Erklärung ist zu stark vereinfacht. Für eine genauere Erklärung müssen Sie sich die vom CPU-Hersteller bereitgestellte Literatur für Assembler-Programmierer und Compiler-Autoren ansehen. Die Wikipedia-Seite zu Branch Predictors ist gut Hintergrund.)


Es gibt jedoch eine Sache, mit der Sie bei dieser Optimierung vorsichtig sein müssen. Gibt es Werte, bei denen a * b != 0 Die falsche Antwort gibt? Betrachten Sie Fälle, in denen die Berechnung des Produkts zu einem Ganzzahlüberlauf führt.


[~ # ~] Update [~ # ~]

Ihre Grafiken bestätigen in der Regel, was ich gesagt habe.

  • Es gibt auch einen "Verzweigungsvorhersage" -Effekt im Fall der bedingten Verzweigung a * b != 0, Und dies wird in den Diagrammen deutlich.

  • Wenn Sie die Kurven über 0,9 hinaus auf die X-Achse projizieren, sieht es so aus, als ob 1) sie sich bei etwa 1,0 und 2) treffen, liegt der Treffpunkt ungefähr auf dem gleichen Y-Wert wie für X = 0,0.


UPDATE 2

Ich verstehe nicht, warum die Kurven für die Fälle a + b != 0 Und a | b != 0 Unterschiedlich sind. Es gibt könnte sein etwas Kluges in der Logik der Verzweigungsvorhersage. Oder es könnte etwas anderes anzeigen.

(Beachten Sie, dass dies für eine bestimmte Chip-Modellnummer oder sogar für eine bestimmte Version spezifisch sein kann. Die Ergebnisse Ihrer Benchmarks können auf anderen Systemen unterschiedlich sein.)

Beide haben jedoch den Vorteil, für alle nicht negativen Werte von a und b zu arbeiten.

233
Stephen C

Ich denke, Ihr Benchmark weist einige Mängel auf und ist möglicherweise nicht nützlich, um auf echte Programme zu schließen. Hier sind meine Gedanken:

  • (a+b)!=0 Macht das Falsche für positive und negative Werte, die sich zu Null summieren. Sie können es also im Allgemeinen nicht verwenden, auch wenn es hier funktioniert.

  • Ebenso wird (a*b)!=0 Für Werte, die überlaufen, das Falsche tun. (Zufälliges Beispiel: 196608 * 327680 ist 0, da das wahre Ergebnis zufällig durch 2 teilbar ist32Die niedrigen 32 Bits sind also 0, und diese Bits sind alles, was Sie erhalten, wenn es sich um eine int -Operation handelt.)

  • (a|b)!=0 Und (a+b)!=0 Testen, ob entweder ein Wert ungleich Null ist, wohingegen a != 0 && b != 0 Und (a*b)!=0 Testen, ob both sind ungleich Null. Sie vergleichen also nicht nur das Timing der Arithmetik: Wenn die Bedingung häufiger zutrifft, werden mehr Ausführungen des Körpers if ausgeführt, was ebenfalls mehr Zeit in Anspruch nimmt.

  • Die Funktion VM optimiert den Ausdruck während der ersten Durchläufe der äußeren Schleife (fraction), wenn fraction 0 ist und die Verzweigungen so gut wie nie ausgeführt werden Das Optimierungsprogramm kann verschiedene Aktionen ausführen, wenn Sie fraction bei 0.5 starten.

  • Wenn die VM hier nicht in der Lage ist, einige der Array-Begrenzungsprüfungen zu eliminieren, enthält der Ausdruck nur aufgrund der Begrenzungsprüfungen vier weitere Verzweigungen Dies kann zu unterschiedlichen Ergebnissen führen, wenn Sie das zweidimensionale Array in zwei flache Arrays aufteilen und nums[0][i] und nums[1][i] in nums0[i] und nums1[i].

  • Prädiktoren für CPU-Zweige erkennen kurze Muster in den Daten oder Läufe aller genommenen oder nicht genommenen Zweige. Ihre zufällig generierten Benchmark-Daten sind das Worst-Case-Szenario für einen Branch Predictor. Wenn reale Daten ein vorhersagbares Muster aufweisen oder lange Läufe von Werten ohne und ohne Null vorliegen, können die Verzweigungen hohe Kosten verursachen geringer, weniger.

  • Der bestimmte Code, der ausgeführt wird, nachdem die Bedingung erfüllt ist, kann sich auf die Leistung beim Auswerten der Bedingung auswirken, da er sich beispielsweise darauf auswirkt, ob die Schleife entrollt werden kann, welche CPU-Register verfügbar sind und ob eines der abgerufenen nums -Werte müssen nach Auswertung der Bedingung wiederverwendet werden. Das bloße Inkrementieren eines Zählers im Benchmark ist kein perfekter Platzhalter für das, was echter Code tun würde.

  • System.currentTimeMillis() ist auf den meisten Systemen nicht genauer als +/- 10 ms. System.nanoTime() ist normalerweise genauer.

Es gibt viele Unsicherheiten, und es ist immer schwer, mit solchen Mikrooptimierungen irgendetwas Bestimmtes zu sagen, weil ein Trick, der auf einem schneller ist VM oder CPU kann auf einem anderen langsamer sein Bei der 32-Bit-HotSpot-JVM gibt es keine 64-Bit-Version, sondern zwei Varianten: beim "Client" VM mit anderen (schwächeren) Optimierungen als beim "Server" VM.

Wenn Sie den von der VM generierten Maschinencode zerlegen können, tun Sie dies, anstatt zu raten, was es tut!

65
Boann

Die Antworten hier sind gut, obwohl ich eine Idee hatte, die die Dinge verbessern könnte.

Da die beiden Verzweigungen und die zugehörige Verzweigungsvorhersage wahrscheinlich die Ursache sind, können wir die Verzweigung möglicherweise auf eine einzelne Verzweigung reduzieren, ohne die Logik überhaupt zu ändern.

bool aNotZero = (nums[0][i] != 0);
bool bNotZero = (nums[1][i] != 0);
if (aNotZero && bNotZero) { /* Some code */ }

Es kann auch funktionieren

int a = nums[0][i];
int b = nums[1][i];
if (a != 0 && b != 0) { /* Some code */ }

Der Grund dafür ist, dass nach den Kurzschlussregeln, wenn der erste Boolesche Wert falsch ist, der zweite nicht ausgewertet werden sollte. Es muss eine zusätzliche Verzweigung durchgeführt werden, um zu vermeiden, dass nums[1][i] wenn nums[0][i] war falsch. Nun, es ist dir vielleicht egal, dass nums[1][i] wird ausgewertet, aber der Compiler kann nicht sicher sein, dass er dabei keinen out of range oder null ref wirft. Indem der if-Block auf einfache Bools reduziert wird, kann der Compiler klug genug sein, um zu erkennen, dass das unnötige Auswerten des zweiten Booleschen keine negativen Nebenwirkungen hat.

23
Pagefault

Wenn wir die Multiplikation nehmen, ist das Produkt 0, auch wenn eine Zahl 0 ist. Beim Schreiben

    (a*b != 0)

Es wertet das Ergebnis des Produkts aus und eliminiert so die ersten Iterationsschritte ab 0. Folglich sind die Vergleiche geringer als bei Vorliegen der Bedingung

   (a != 0 && b != 0)

Dabei wird jedes Element mit 0 verglichen und ausgewertet. Der Zeitaufwand ist daher geringer. Aber ich glaube, dass die zweite Bedingung Ihnen eine genauere Lösung geben könnte.

10
Sanket Gupte

Sie verwenden randomisierte Eingabedaten, wodurch die Zweige unvorhersehbar werden. In der Praxis sind Verzweigungen oft (~ 90%) vorhersehbar, so dass in echtem Code der Verzweigungscode wahrscheinlich schneller ist.

Das gesagt. Ich verstehe nicht, wie a*b != 0 kann schneller sein als (a|b) != 0. Im Allgemeinen ist eine ganzzahlige Multiplikation teurer als ein bitweises ODER. Aber solche Dinge werden gelegentlich komisch. Siehe zum Beispiel das Beispiel "Beispiel 7: Hardware-Komplexität" aus Galerie der Prozessor-Cache-Effekte .

8
StackedCrooked