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
?
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% 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:
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!
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:
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}
:
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:
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.
Tags und Links optimization c++ c++11 boolean micro-optimization