Insbesondere, wenn ich eine Reihe von if
... else if
Anweisungen habe und die relative Wahrscheinlichkeit, mit der jede Anweisung bewertet wird, im Voraus irgendwie weiß true
, wie groß ist der Unterschied in der Ausführungszeit, wenn sie nach Wahrscheinlichkeit sortiert werden? Soll ich das zum Beispiel bevorzugen:
if (highly_likely)
//do something
else if (somewhat_likely)
//do something
else if (unlikely)
//do something
dazu?:
if (unlikely)
//do something
else if (somewhat_likely)
//do something
else if (highly_likely)
//do something
Es scheint offensichtlich, dass die sortierte Version schneller wäre, aber aus Gründen der Lesbarkeit oder des Vorhandenseins von Nebenwirkungen möchten wir sie möglicherweise nicht optimal bestellen. Es ist auch schwer zu sagen, wie gut die CPU mit der Verzweigungsvorhersage umgehen kann, bis Sie den Code tatsächlich ausführen.
Während ich damit experimentierte, beantwortete ich meine eigene Frage für einen bestimmten Fall, aber ich würde auch gerne andere Meinungen / Erkenntnisse hören.
Wichtig: Bei dieser Frage wird davon ausgegangen, dass die if
Anweisungen beliebig neu angeordnet werden können, ohne dass sich dies auf das Verhalten des Programms auswirkt. In meiner Antwort schließen sich die drei bedingten Tests gegenseitig aus und verursachen keine Nebenwirkungen. Wenn die Aussagen in einer bestimmten Reihenfolge ausgewertet werden müssen, um ein gewünschtes Verhalten zu erzielen, ist das Thema Effizienz sicherlich umstritten.
Antworten:
In der Regel gehen die meisten, wenn nicht alle Intel-CPUs davon aus, dass Vorwärtszweige nicht beim ersten Mal verwendet werden. Siehe Godbolts Arbeit .
Danach wird die Verzweigung in einen Verzweigungsvorhersage-Cache verschoben, und das vergangene Verhalten wird verwendet, um die zukünftige Verzweigungsvorhersage zu informieren.
In einer engen Schleife wird der Effekt einer Fehlordnung also relativ gering sein. Der Zweigprädiktor wird lernen, welche Gruppe von Zweigen am wahrscheinlichsten ist, und wenn Sie nicht triviale Menge an Arbeit in der Schleife haben, summieren sich die kleinen Unterschiede nicht viel.
Im Allgemeinen bestellen die meisten Compiler standardmäßig (ohne einen anderen Grund) den erzeugten Maschinencode ungefähr so, wie Sie ihn in Ihrem Code bestellt haben. Wenn also Anweisungen Forward-Zweige sind, wenn sie fehlschlagen.
Sie sollten Ihre Zweige daher in der Reihenfolge abnehmender Wahrscheinlichkeit ordnen, um die beste Verzweigungsvorhersage aus einer "ersten Begegnung" zu erhalten.
Ein Mikrobenchmark, das viele Male eine enge Schleife über eine Reihe von Bedingungen durchläuft und triviale Arbeit leistet, wird von winzigen Effekten der Befehlsanzahl und dergleichen dominiert und wenig von relativen Verzweigungsvorhersageproblemen. In diesem Fall müssen Sie also ein Profil erstellen , da Faustregeln nicht zuverlässig sind.
Darüber hinaus gelten die Vektorisierung und viele andere Optimierungen für winzige enge Schleifen.
Fügen Sie also im Allgemeinen Code den wahrscheinlichsten Code in den
if
Block ein, und dies führt zu den wenigsten nicht zwischengespeicherten Verzweigungsvorhersagefehlern. Befolgen Sie in engen Schleifen die allgemeine Regel, um zu beginnen, und wenn Sie mehr wissen müssen, haben Sie keine andere Wahl, als ein Profil zu erstellen.Natürlich geht das alles aus dem Fenster, wenn einige Tests weitaus billiger sind als andere.
quelle
Ich habe den folgenden Test durchgeführt, um die Ausführung von zwei verschiedenen
if
...else if
Blöcken zu planen, von denen einer nach Wahrscheinlichkeit sortiert und der andere in umgekehrter Reihenfolge sortiert ist:Bei Verwendung von MSVC2017 mit / O2 zeigen die Ergebnisse, dass die sortierte Version durchweg etwa 28% schneller ist als die unsortierte Version. Gemäß dem Kommentar von luk32 habe ich auch die Reihenfolge der beiden Tests geändert, was einen spürbaren Unterschied macht (22% gegenüber 28%). Der Code wurde unter Windows 7 auf einem Intel Xeon E5-2697 v2 ausgeführt. Dies ist natürlich sehr problemspezifisch und sollte nicht als schlüssige Antwort interpretiert werden.
quelle
if... else if
Anweisung einen erheblichen Einfluss darauf haben kann, wie Logik durch den Code fließt. Dieunlikely
Überprüfung wird möglicherweise nicht häufig durchgeführt, es kann jedoch erforderlich sein, dass das Unternehmenunlikely
zuerst den Zustand überprüft, bevor es nach anderen überprüft.g++ -O2 -march=native -std=c++14
gibt den sortierten bedingten Anweisungen eine leichte Kante, aber die meiste Zeit betrug der prozentuale Unterschied zwischen den beiden Läufen ~ 5%. Mehrmals war es tatsächlich langsamer (aufgrund von Abweichungen). Ich bin mir ziemlich sicher, dassif
es sich nicht lohnt, sich darüber Sorgen zu machen. PGO wird wahrscheinlich solche Fälle vollständig behandelnNein, sollten Sie nicht, es sei denn, Sie sind wirklich sicher, dass das Zielsystem betroffen ist. Standardmäßig ist die Lesbarkeit aktiviert.
Ich bezweifle Ihre Ergebnisse sehr. Ich habe Ihr Beispiel ein wenig geändert, damit das Umkehren der Ausführung einfacher ist. Ideone zeigt ziemlich konsequent, dass die umgekehrte Reihenfolge schneller ist, wenn auch nicht viel. Bei bestimmten Läufen drehte sich sogar dies gelegentlich um. Ich würde sagen, die Ergebnisse sind nicht schlüssig. coliru meldet auch keinen wirklichen Unterschied. Ich kann später die Exynos5422-CPU auf meinem Odroid xu4 überprüfen.
Die Sache ist, dass moderne CPUs Verzweigungsprädiktoren haben. Es gibt viel Logik, die sowohl dem Vorabrufen von Daten als auch von Anweisungen gewidmet ist, und moderne x86-CPUs sind in dieser Hinsicht ziemlich intelligent. Einige schlankere Architekturen wie ARMs oder GPUs sind möglicherweise dafür anfällig. Aber es hängt sehr stark vom Compiler und vom Zielsystem ab.
Ich würde sagen, dass die Optimierung der Filialreihenfolge ziemlich fragil und kurzlebig ist. Tun Sie dies nur als einen wirklich feinen Abstimmungsschritt.
Code:
quelle
Nur meine 5 Cent. Es scheint die Auswirkung der Bestellung zu sein, wenn Aussagen abhängen sollten von:
Wahrscheinlichkeit jeder if-Anweisung.
Anzahl der Iterationen, damit der Verzweigungsprädiktor einschalten kann.
Wahrscheinliche / unwahrscheinliche Compiler-Hinweise, dh Code-Layout.
Um diese Faktoren zu untersuchen, habe ich die folgenden Funktionen verglichen:
order_ifs ()
reversed_ifs ()
ordered_ifs_with_hints ()
reverse_ifs_with_hints ()
Daten
Das Datenarray enthält Zufallszahlen zwischen 0 und 100:
Die Ergebnisse
Die folgenden Ergebnisse gelten für Intel i5 bei 3,2 GHz und G ++ 6.3.0. Das erste Argument ist der Prüfpunkt (dh die Wahrscheinlichkeit in %% für die höchstwahrscheinliche if-Anweisung), das zweite Argument ist data_sz (dh die Anzahl der Iterationen).
Analyse
1. Die Bestellung ist wichtig
Bei 4K-Iterationen und einer (fast) 100% igen Wahrscheinlichkeit einer sehr beliebten Aussage beträgt der Unterschied 223%:
Bei 4K-Iterationen und einer Wahrscheinlichkeit von 50% für eine sehr beliebte Aussage beträgt der Unterschied etwa 14%:
2. Die Anzahl der Iterationen spielt eine Rolle
Der Unterschied zwischen 4K- und 8K-Iterationen für eine (fast) 100% ige Wahrscheinlichkeit einer sehr beliebten Aussage beträgt ungefähr das Zweifache (wie erwartet):
Der Unterschied zwischen 4K- und 8K-Iterationen für eine 50% ige Wahrscheinlichkeit einer sehr beliebten Aussage beträgt jedoch das 5,5-fache:
Warum ist das so? Wegen Branch Predictor Misses. Hier sind die Verzweigungsfehler für jeden oben genannten Fall:
Auf meinem i5 schlägt der Zweigprädiktor für nicht so wahrscheinliche Zweige und große Datenmengen spektakulär fehl.
3. Hinweise helfen ein bisschen
Bei 4K-Iterationen sind die Ergebnisse bei einer Wahrscheinlichkeit von 50% etwas schlechter und bei einer Wahrscheinlichkeit von nahezu 100% etwas besser:
Bei 8K-Iterationen sind die Ergebnisse jedoch immer etwas besser:
Die Hinweise helfen also auch, aber nur ein kleines bisschen.
Die allgemeine Schlussfolgerung lautet: Benchmarking des Codes immer, da die Ergebnisse überraschen können.
Hoffentlich hilft das.
quelle
g++ -O2
oder verwendet-O3 -fno-tree-vectorize
, aber Sie sollten es sagen.Basierend auf einigen der anderen Antworten hier sieht es so aus, als ob die einzige wirkliche Antwort lautet: Es kommt darauf an . Es hängt mindestens von Folgendem ab (wenn auch nicht unbedingt in dieser Reihenfolge der Wichtigkeit):
Die einzige Möglichkeit, dies mit Sicherheit zu wissen, besteht darin, Ihren speziellen Fall zu bewerten, vorzugsweise auf einem System, das mit dem beabsichtigten System identisch (oder diesem sehr ähnlich) ist, auf dem der Code schließlich ausgeführt wird. Wenn es auf einer Reihe unterschiedlicher Systeme mit unterschiedlicher Hardware, unterschiedlichem Betriebssystem usw. ausgeführt werden soll, empfiehlt es sich, mehrere Varianten zu vergleichen, um festzustellen, welche am besten geeignet sind. Es kann sogar eine gute Idee sein, den Code mit einer Bestellung auf einem Systemtyp und einer anderen Bestellung auf einem anderen Systemtyp kompilieren zu lassen.
Meine persönliche Faustregel (in den meisten Fällen ohne Benchmark) lautet: Bestellen auf der Grundlage von:
quelle
Die Art und Weise, wie ich dies normalerweise für Hochleistungscode gelöst sehe, besteht darin, die Reihenfolge beizubehalten, die am besten lesbar ist, aber dem Compiler Hinweise zu geben. Hier ist ein Beispiel aus dem Linux-Kernel :
Hier wird davon ausgegangen, dass die Zugriffsprüfung bestanden wird und kein Fehler zurückgegeben wird
res
. Der Versuch, eine dieser if-Klauseln neu zu ordnen, würde den Code nur verwirren, aber daslikely()
undunlikely()
Makros verbessern tatsächlich die Lesbarkeit, indem sie darauf hinweisen, was der Normalfall und was die Ausnahme ist.Die Linux-Implementierung dieser Makros verwendet GCC-spezifische Funktionen . Es scheint, dass Clang und Intel C Compiler dieselbe Syntax unterstützen, aber MSVC verfügt nicht über eine solche Funktion .
quelle
likely()
undunlikely()
definiert sind, und einige Informationen zur entsprechenden Compilerfunktion enthalten könnten .else if
wenn der Compiler nicht klug genug ist, um zu wissen, dass sich die Bedingungen gegenseitig ausschließen.Hängt auch von Ihrem Compiler und der Plattform ab, für die Sie kompilieren.
Theoretisch sollte die wahrscheinlichste Bedingung dazu führen, dass die Steuerung so wenig wie möglich springt.
Normalerweise sollte die wahrscheinlichste Bedingung die erste sein:
Die beliebtesten asm der auf bedingte Verzweigungen basiert , die springen , wenn die Bedingung ist wahr . Dieser C-Code wird wahrscheinlich in einen solchen Pseudoasmus übersetzt:
Dies liegt daran, dass die CPU durch Sprünge die Ausführungspipeline abbricht und blockiert, weil sich der Programmzähler geändert hat (für Architekturen, die wirklich häufige Pipelines unterstützen). Dann geht es um den Compiler, der einige ausgefeilte Optimierungen anwenden kann oder nicht, um die statistisch wahrscheinlichste Bedingung zu haben, dass die Steuerung weniger Sprünge macht.
quelle
clang
fürtest2
undtest3
: aufgrund von Heuristiken, die darauf hinweisen, dass ein< 0
oder ein== 0
Test wahrscheinlich falsch ist , tatsächlich ein anderer Ansatz gewählt wurde , wurde beschlossen, den Rest der Funktion auf beiden Pfaden zu klonen, damitcondition == false
der Fall durch den Pfad erfolgen kann. Dies ist nur möglich, weil der Rest der Funktion kurz ist:test4
Ich habe eine weitere Operation hinzugefügt und es geht zurück zu dem oben beschriebenen Ansatz.jmp
nicht vorhanden ist Nützlich, damit die Abruf- / Dekodierungsbandbreite verschwendet wird (2), selbst wenn vorausgesagt wird, dass moderne große Kerne nur einen Abruf pro Zyklus ausführen, sodass eine feste Grenze von 1 genommenem Zweig / Zyklus festgelegt ist (OTOH Modern Intel kann 2 nicht genommene / Zyklen ausführen) (3) ) Es ist schwieriger für dieIch habe beschlossen, den Test auf meinem eigenen Computer mit Lik32-Code erneut auszuführen. Ich musste es ändern, weil mein Windows- oder Compiler dachte, dass eine hohe Auflösung 1 ms beträgt
mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -fexceptions -g
GCC hat für beide Originalcodes dieselbe Transformation durchgeführt.
Beachten Sie, dass nur die beiden ersten Bedingungen getestet werden, da die dritte immer wahr sein muss. GCC ist hier eine Art Sherlock.
Umkehren
Das sagt uns also nicht viel, außer dass der letzte Fall keine Verzweigungsvorhersage benötigt.
Jetzt habe ich alle 6 Kombinationen der Ifs ausprobiert, die Top 2 sind die Originalumkehrung und sortiert. hoch ist> = 95, niedrig ist <20, mittel ist 20-94 mit jeweils 10000000 Iterationen.
Warum ist die Reihenfolge hoch, niedrig, med dann schneller (geringfügig)?
Weil das Unvorhersehbarste das Letzte ist und daher niemals durch einen Verzweigungsprädiktor geführt wird.
So werden die Zweige vorhergesagt genommen, genommen und der Rest mit
6% + (0,94 *) 20% falsche Vorhersagen.
"Sortiert"
Die Zweige werden mit nicht genommen, nicht genommen und Sherlock vorhergesagt.
25% + (0,75 *) 24% falsche Vorhersagen
Geben Sie eine Differenz von 18-23% an (gemessene Differenz von ~ 9%), aber wir müssen Zyklen berechnen, anstatt% falsch vorherzusagen.
Nehmen wir an, dass auf meiner Nehalem-CPU eine Strafe von 17 Zyklen falsch vorhergesagt wird und dass jede Überprüfung 1 Zyklus dauert (4-5 Anweisungen) und die Schleife auch einen Zyklus dauert. Die Datenabhängigkeiten sind die Zähler und die Schleifenvariablen, aber sobald die falschen Vorhersagen aus dem Weg sind, sollte dies das Timing nicht beeinflussen.
Für "Umkehren" erhalten wir also die Timings (dies sollte die in der Computerarchitektur verwendete Formel sein: Ein quantitativer Ansatz IIRC).
und das gleiche für "sortiert"
(8,26-7,24) / 8,26 = 13,8% gegenüber ~ 9% gemessen (nahe am gemessenen!?!).
Das Offensichtliche des OP ist also nicht offensichtlich.
Bei diesen Tests sind andere Tests mit komplizierterem Code oder mehr Datenabhängigkeiten sicherlich anders. Messen Sie also Ihren Fall.
Durch Ändern der Testreihenfolge wurden die Ergebnisse geändert. Dies kann jedoch an unterschiedlichen Ausrichtungen des Schleifenstarts liegen, die idealerweise 16 Byte betragen sollten, die auf allen neueren Intel-CPUs ausgerichtet sind, in diesem Fall jedoch nicht.
quelle
Ordnen Sie sie in einer beliebigen logischen Reihenfolge an. Sicher, die Verzweigung ist möglicherweise langsamer, aber die Verzweigung sollte nicht den größten Teil der Arbeit Ihres Computers ausmachen.
Wenn Sie an einem leistungskritischen Teil des Codes arbeiten, verwenden Sie sicherlich logische Reihenfolge, profilgesteuerte Optimierung und andere Techniken, aber für allgemeinen Code denke ich, dass dies eher eine stilistische Wahl ist.
quelle
i++
wann++i
dies der Fall wäre, da mir bewusst ist, dass esi++
für einige Iteratoren schwierig ist, bis zu optimieren,++i
und der Unterschied (für mich) keine Rolle spielt. Hier geht es darum, Pessimisierung zu vermeiden. Wenn Sie den wahrscheinlichsten Block als Standardgewohnheit an die erste Stelle setzen, wird dies nicht zu einer spürbaren Verringerung der Lesbarkeit führen (und möglicherweise sogar helfen!). Dies führt zu Code, der für die Verzweigungsvorhersage geeignet ist (und Ihnen somit einen einheitlichen kleinen Leistungsschub bietet, der nicht wieder erfasst werden kann) durch spätereWenn Sie die relative Wahrscheinlichkeit einer if-else-Anweisung bereits kennen, ist es für Leistungszwecke besser, die sortierte Methode zu verwenden, da nur eine Bedingung (die wahre) überprüft wird.
Auf unsortierte Weise überprüft der Compiler alle Bedingungen unnötig und nimmt sich Zeit.
quelle