Sie sind ein Opfer des Fehlschlags der Zweigvorhersage.
Was ist Zweigvorhersage?
Betrachten Sie einen Eisenbahnknotenpunkt:
Bild von Mecanismo, über Wikimedia Commons. Wird unter der CC-By-SA 3.0- Lizenz verwendet.
Nehmen wir zum Zwecke der Argumentation an, dass dies im 19. Jahrhundert war - vor Ferngesprächen oder Funkkommunikation.
Sie sind der Betreiber einer Kreuzung und hören einen Zug kommen. Sie haben keine Ahnung, in welche Richtung es gehen soll. Sie halten den Zug an, um den Fahrer zu fragen, in welche Richtung er möchte. Und dann stellen Sie den Schalter entsprechend ein.
Züge sind schwer und haben viel Trägheit. Es dauert also ewig, bis sie anfangen und langsamer werden.
Gibt es einen besseren Weg? Sie raten, in welche Richtung der Zug fahren wird!
- Wenn Sie richtig geraten haben, geht es weiter.
- Wenn Sie falsch geraten haben, hält der Kapitän an, fährt zurück und schreit Sie an, um den Schalter umzulegen. Dann kann es auf dem anderen Pfad neu starten.
Wenn Sie jedes Mal richtig raten , muss der Zug niemals anhalten.
Wenn Sie zu oft falsch raten , verbringt der Zug viel Zeit damit, anzuhalten, zu sichern und neu zu starten.
Betrachten Sie eine if-Anweisung: Auf Prozessorebene handelt es sich um eine Verzweigungsanweisung:
Sie sind ein Prozessor und sehen einen Zweig. Sie haben keine Ahnung, in welche Richtung es gehen wird. Wie geht's? Sie stoppen die Ausführung und warten, bis die vorherigen Anweisungen vollständig sind. Dann gehen Sie den richtigen Weg weiter.
Moderne Prozessoren sind kompliziert und haben lange Pipelines. Es dauert also ewig, bis sie sich "aufgewärmt" und "verlangsamt" haben.
Gibt es einen besseren Weg? Sie raten, in welche Richtung der Zweig gehen wird!
- Wenn Sie richtig geraten haben, fahren Sie mit der Ausführung fort.
- Wenn Sie falsch geraten haben, müssen Sie die Pipeline spülen und zum Zweig zurückrollen. Dann können Sie den anderen Pfad neu starten.
Wenn Sie jedes Mal richtig raten , muss die Ausführung niemals aufhören.
Wenn Sie zu oft falsch raten , verbringen Sie viel Zeit damit, anzuhalten, zurückzurollen und neu zu starten.
Dies ist eine Verzweigungsvorhersage. Ich gebe zu, es ist nicht die beste Analogie, da der Zug die Richtung nur mit einer Flagge signalisieren könnte. Bei Computern weiß der Prozessor jedoch bis zum letzten Moment nicht, in welche Richtung ein Zweig gehen wird.
Wie würden Sie strategisch raten, um die Häufigkeit zu minimieren, mit der der Zug auf dem anderen Weg zurückfahren und fahren muss? Sie schauen auf die Vergangenheit! Wenn der Zug 99% der Zeit nach links fährt, raten Sie nach links. Wenn es sich abwechselt, wechseln Sie Ihre Vermutungen. Wenn es alle drei Male in eine Richtung geht, raten Sie dasselbe ...
Mit anderen Worten, Sie versuchen, ein Muster zu identifizieren und ihm zu folgen. So funktionieren Zweigprädiktoren mehr oder weniger.
Die meisten Anwendungen haben gut erzogene Zweige. Moderne Branchenprädiktoren erzielen daher in der Regel Trefferquoten von> 90%. Bei unvorhersehbaren Verzweigungen ohne erkennbare Muster sind Verzweigungsvorhersagen jedoch praktisch nutzlos.
Weiterführende Literatur: Artikel "Branch Predictor" auf Wikipedia .
Wie von oben angedeutet, ist der Schuldige diese if-Aussage:
if (data[c] >= 128)
sum += data[c];
Beachten Sie, dass die Daten gleichmäßig zwischen 0 und 255 verteilt sind. Wenn die Daten sortiert werden, wird ungefähr die erste Hälfte der Iterationen nicht in die if-Anweisung eingegeben. Danach geben alle die if-Anweisung ein.
Dies ist für den Zweigprädiktor sehr freundlich, da der Zweig viele Male nacheinander in dieselbe Richtung geht. Selbst ein einfacher Sättigungszähler sagt den Zweig bis auf die wenigen Iterationen nach dem Richtungswechsel korrekt voraus.
Schnelle Visualisierung:
T = branch taken
N = branch not taken
data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N N N N N ... N N T T T ... T T T ...
= NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT (easy to predict)
Wenn die Daten jedoch vollständig zufällig sind, wird der Verzweigungsprädiktor unbrauchbar, da er keine zufälligen Daten vorhersagen kann. Daher wird es wahrscheinlich eine Fehlvorhersage von etwa 50% geben (nicht besser als zufälliges Erraten).
data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118, 14, 150, 177, 182, 133, ...
branch = T, T, N, T, T, T, T, N, T, N, N, T, T, T, N ...
= TTNTTTTNTNNTTTN ... (completely random - hard to predict)
Was kann also getan werden?
Wenn der Compiler den Zweig nicht in eine bedingte Verschiebung optimieren kann, können Sie einige Hacks versuchen, wenn Sie bereit sind, die Lesbarkeit für die Leistung zu opfern.
Ersetzen:
if (data[c] >= 128)
sum += data[c];
mit:
int t = (data[c] - 128) >> 31;
sum += ~t & data[c];
Dies eliminiert den Zweig und ersetzt ihn durch einige bitweise Operationen.
(Beachten Sie, dass dieser Hack nicht unbedingt der ursprünglichen if-Anweisung entspricht. In diesem Fall gilt er jedoch für alle Eingabewerte von data[]
.)
Benchmarks: Core i7 920 bei 3,5 GHz
C ++ - Visual Studio 2010 - x64-Version
// Branch - Random
seconds = 11.777
// Branch - Sorted
seconds = 2.352
// Branchless - Random
seconds = 2.564
// Branchless - Sorted
seconds = 2.587
Java - NetBeans 7.1.1 JDK 7 - x64
// Branch - Random
seconds = 10.93293813
// Branch - Sorted
seconds = 5.643797077
// Branchless - Random
seconds = 3.113581453
// Branchless - Sorted
seconds = 3.186068823
Beobachtungen:
- Mit der Verzweigung: Es gibt einen großen Unterschied zwischen sortierten und unsortierten Daten.
- Mit dem Hack: Es gibt keinen Unterschied zwischen sortierten und unsortierten Daten.
- Im C ++ - Fall ist der Hack tatsächlich etwas langsamer als beim Verzweigen, wenn die Daten sortiert werden.
Eine allgemeine Faustregel besteht darin, eine datenabhängige Verzweigung in kritischen Schleifen (wie in diesem Beispiel) zu vermeiden.
Aktualisieren:
GCC 4.6.1 mit -O3
oder -ftree-vectorize
auf x64 kann eine bedingte Verschiebung generieren. Es gibt also keinen Unterschied zwischen sortierten und unsortierten Daten - beide sind schnell.
(Oder etwas schnell: Für den bereits sortierten Fall cmov
kann er langsamer sein, insbesondere wenn GCC ihn auf den kritischen Pfad stellt, anstatt nur add
, insbesondere bei Intel vor Broadwell, wo cmov
eine Latenz von 2 Zyklen vorliegt : Das gcc-Optimierungsflag -O3 macht den Code langsamer als -O2 )
VC ++ 2010 kann auch unter keine bedingten Verschiebungen für diesen Zweig generieren /Ox
.
Intel C ++ Compiler (ICC) 11 macht etwas Wunderbares. Es vertauscht die beiden Schleifen und hebt dadurch den unvorhersehbaren Zweig zur äußeren Schleife. Es ist also nicht nur immun gegen falsche Vorhersagen, sondern auch doppelt so schnell wie alles, was VC ++ und GCC erzeugen können! Mit anderen Worten, ICC nutzte die Testschleife, um den Benchmark zu besiegen ...
Wenn Sie dem Intel-Compiler den verzweigungslosen Code geben, vektorisiert er ihn einfach nach rechts ... und ist genauso schnell wie bei der Verzweigung (mit dem Schleifenaustausch).
Dies zeigt, dass selbst ausgereifte moderne Compiler in ihrer Fähigkeit, Code zu optimieren, sehr unterschiedlich sein können ...