Verwenden von Boolen in Berechnungen, um Verzweigungen zu vermeiden

8

Hier ist eine kleine Mikro-Optimierungs-Neugier, die ich mir ausgedacht habe:

%Vor%

Es scheint, dass die beiden Methoden praktisch dasselbe tun. Vermeidet die zweite Version eine Verzweigung (und ist folglich schneller als die erste Version) oder kann ein Compiler diese Art der Optimierung mit -O3 ?

durchführen?     
Vittorio Romeo 23.11.2013, 15:02
quelle

2 Antworten

3

Ja, Ihr Trick erlaubt es, Verzweigungen zu vermeiden und macht es schneller ... manchmal.

Ich habe einen Benchmark geschrieben, der diese Lösungen in verschiedenen Situationen vergleicht, zusammen mit meinen eigenen:

%Vor%

Meine Ergebnisse sind folgende:

%Vor%

Off ist wenn die Timer ausgeschaltet sind. In diesem Fall nehmen die Lösungen ungefähr die gleiche Zeit in Anspruch.

On ist, wenn sie aktiviert sind. Verzweigungslösung zweimal schneller.

Pattern ist, wenn sie sich im 100110-Muster befinden. Die Leistung ist ähnlich, aber die Verzweigung ist ein bisschen schneller.

Random ist, wenn die Verzweigung unvorhersehbar ist. In diesem Fall ist Multiplikationen mehr als 2 mal schneller.

In allen Fällen ist mein Bit-Hacking-Trick am schnellsten, außer für On , wo die Verzweigung gewinnt.

Beachten Sie, dass dieser Benchmark nicht unbedingt für alle Prozessoren der Compilerversion usw. repräsentativ ist. Selbst kleine Änderungen des Benchmarks können die Ergebnisse auf den Kopf stellen (zum Beispiel wenn der Compiler inline wissen kann mStepSize ist 1 als Multiplikation kann tatsächlich am schnellsten sein) .

Code der Benchmark:

%Vor%     
zch 23.11.2013, 16:52
quelle
2

Does the second version avoid a branch

Wenn Sie Ihren Code kompilieren, um die Assembler-Ausgabe zu erhalten, g++ -o test.s test.cpp -S , werden Sie feststellen, dass in der zweiten Funktion tatsächlich eine Verzweigung vermieden wird.

and consequently, is faster than the first version

Ich habe jede Ihrer Funktionen 2147483647 oder INT_MAX Anzahl von Malen, wo ich in jeder Iteration einen booleschen Wert nach running Mitglied Ihrer Timer Struktur zugewiesen, mit diesem Code:

%Vor%

und das sind die Ergebnisse, die ich bekommen habe:

%Vor%

ja, die zweite Funktion ist deutlich schneller und um etwa 35%. Beachten Sie, dass die prozentuale Zunahme der zeitgesteuerten Leistung bei einer kleineren Anzahl von Iterationen zwischen 30 und 55 Prozent schwankte, während sie bei etwa 35% zu liegen scheint, je länger sie läuft. Dies ist möglicherweise auf eine sporadische Ausführung von Systemaufgaben während der Simulation zurückzuführen, die viel weniger sporadisch wird, dh konsistent, je länger Sie die Simulation ausführen (obwohl dies nur meine Annahme ist, ich habe keine Ahnung, ob sie tatsächlich wahr ist) / p>

Alles in allem, nette Frage, ich habe heute etwas gelernt!

MEHR:

natürlich, indem wir running zufällig erzeugen, machen wir die Verzweigungsvorhersage im Wesentlichen in der ersten Funktion nutzlos, so dass die obigen Ergebnisse nicht zu überraschend sind. Wenn wir jedoch entscheiden, running während Schleifeniterationen nicht zu ändern und stattdessen den Standardwert zu belassen, in diesem Fall false , wird die Verzweigungsvorhersage in der ersten Funktion ihre Wirkung entfalten und tatsächlich um fast 20% schneller sein. wie diese Ergebnisse vorschlagen:

%Vor%

Da running während der gesamten Ausführung konstant ist, beachten Sie, dass die Simulationszeit viel kürzer ist als bei einer zufälligen Änderung von running - Ergebnis einer Compiler-Optimierung wahrscheinlich.

Warum ist die zweite Funktion in diesem Fall langsamer? Nun, die Verzweigungsprognose wird schnell erkennen, dass die Bedingung in der ersten Funktion niemals erfüllt wird, und wird daher an der ersten Stelle aufhören zu prüfen (als ob if(running) ticks += mStepSize; gar nicht da wäre). Andererseits muss die zweite Funktion diese Anweisung ticks += mStepSize * static_cast<int>(running); bei jeder Iteration ausführen, wodurch die erste Funktion effizienter wird.

Aber was ist, wenn wir running auf true setzen? Nun, die Verzweigungsvorhersage wird wieder anspringen, aber diesmal muss die erste Funktion ticks += mStepSize; in jeder Iteration auswerten; hier die Ergebnisse, wenn running{true} :

%Vor%

Beachten Sie, dass step_versionTwo eine konsistente Zeit benötigt, unabhängig davon, ob running konstant true oder false ist. aber es dauert immer noch länger als step_versionTwo , jedoch marginal. Nun, das könnte daran liegen, dass ich zu faul war, es oft zu testen, um festzustellen, ob es immer schneller ist oder ob es ein einmaliger Zufall war (die Ergebnisse variieren jedes Mal, wenn das Betriebssystem ausgeführt wird) und es wird nicht immer das Gleiche tun). aber wenn es konsistent schneller ist, könnte es daran liegen, dass Funktion zwei ( ticks += mStepSize * static_cast<int>(running); ) eine arithmetische Operation mehr als Funktion eins ( ticks += mStepSize; ) hat.

Schließlich, lassen Sie uns mit einer Optimierung kompilieren - g++ -o test test.cpp -std=c++11 -O1 und lassen Sie uns running zurück zu false und dann überprüfen Sie die Ergebnisse:

%Vor%

mehr oder weniger gleich. der Compiler wird seinen Optimierungsdurchlauf durchführen, und realisieren running ist immer false und wird somit den Körper von step_versionOne entfernen, also wenn Sie ihn aus der Schleife in main aufrufen Ruf einfach die Funktion an und kehre zurück.

andererseits wird bei der Optimierung der zweiten Funktion erkannt, dass ticks += mStepSize * static_cast<int>(running); immer das gleiche Ergebnis erzeugt, d. h. 0 , so dass es auch nicht störend ist, dies auszuführen.

alles in allem, wenn ich richtig bin (und wenn nicht, bitte korrigieren Sie mich, ich bin ziemlich neu in diesem), alles, was Sie erhalten, wenn Sie beide Funktionen von der main Schleife aufrufen, ist ihr Overhead.

ps. Hier ist das Ergebnis für den ersten Fall ( running wird in jeder Iteration zufällig generiert), wenn es mit einer Optimierung kompiliert wird.

%Vor%     
stellarossa 23.11.2013 16:26
quelle