Ist eine switch
Aussage tatsächlich schneller als eine if
Aussage?
Ich habe den folgenden Code auf dem x64 C ++ - Compiler von Visual Studio 2010 mit dem folgenden /Ox
Flag ausgeführt:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_COUNT (1 << 29)
size_t counter = 0;
size_t testSwitch()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
switch (counter % 4 + 1)
{
case 1: counter += 4; break;
case 2: counter += 3; break;
case 3: counter += 2; break;
case 4: counter += 1; break;
}
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
size_t testIf()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
const size_t c = counter % 4 + 1;
if (c == 1) { counter += 4; }
else if (c == 2) { counter += 3; }
else if (c == 3) { counter += 2; }
else if (c == 4) { counter += 1; }
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
int main()
{
printf("Starting...\n");
printf("Switch statement: %u ms\n", testSwitch());
printf("If statement: %u ms\n", testIf());
}
und bekam diese Ergebnisse:
Switch-Anweisung: 5261 ms
If-Anweisung: 5196 ms
Nach dem, was ich gelernt habe, verwenden switch
Anweisungen anscheinend Sprungtabellen, um die Verzweigung zu optimieren.
Fragen:
Wie würde eine einfache Sprungtabelle in x86 oder x64 aussehen?
Verwendet dieser Code eine Sprungtabelle?
Warum gibt es in diesem Beispiel keinen Leistungsunterschied? Gibt es eine Situation , in der es ist ein signifikanter Unterschied in der Leistung?
Demontage des Codes:
testIf:
13FE81B10 sub rsp,48h
13FE81B14 call qword ptr [__imp_clock (13FE81128h)]
13FE81B1A mov dword ptr [start],eax
13FE81B1E mov qword ptr [i],0
13FE81B27 jmp testIf+26h (13FE81B36h)
13FE81B29 mov rax,qword ptr [i]
13FE81B2E inc rax
13FE81B31 mov qword ptr [i],rax
13FE81B36 cmp qword ptr [i],20000000h
13FE81B3F jae testIf+0C3h (13FE81BD3h)
13FE81B45 xor edx,edx
13FE81B47 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B4E mov ecx,4
13FE81B53 div rax,rcx
13FE81B56 mov rax,rdx
13FE81B59 inc rax
13FE81B5C mov qword ptr [c],rax
13FE81B61 cmp qword ptr [c],1
13FE81B67 jne testIf+6Dh (13FE81B7Dh)
13FE81B69 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B70 add rax,4
13FE81B74 mov qword ptr [counter (13FE835D0h)],rax
13FE81B7B jmp testIf+0BEh (13FE81BCEh)
13FE81B7D cmp qword ptr [c],2
13FE81B83 jne testIf+89h (13FE81B99h)
13FE81B85 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B8C add rax,3
13FE81B90 mov qword ptr [counter (13FE835D0h)],rax
13FE81B97 jmp testIf+0BEh (13FE81BCEh)
13FE81B99 cmp qword ptr [c],3
13FE81B9F jne testIf+0A5h (13FE81BB5h)
13FE81BA1 mov rax,qword ptr [counter (13FE835D0h)]
13FE81BA8 add rax,2
13FE81BAC mov qword ptr [counter (13FE835D0h)],rax
13FE81BB3 jmp testIf+0BEh (13FE81BCEh)
13FE81BB5 cmp qword ptr [c],4
13FE81BBB jne testIf+0BEh (13FE81BCEh)
13FE81BBD mov rax,qword ptr [counter (13FE835D0h)]
13FE81BC4 inc rax
13FE81BC7 mov qword ptr [counter (13FE835D0h)],rax
13FE81BCE jmp testIf+19h (13FE81B29h)
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)]
13FE81BD9 sub eax,dword ptr [start]
13FE81BDD imul eax,eax,3E8h
13FE81BE3 cdq
13FE81BE4 mov ecx,3E8h
13FE81BE9 idiv eax,ecx
13FE81BEB cdqe
13FE81BED add rsp,48h
13FE81BF1 ret
testSwitch:
13FE81C00 sub rsp,48h
13FE81C04 call qword ptr [__imp_clock (13FE81128h)]
13FE81C0A mov dword ptr [start],eax
13FE81C0E mov qword ptr [i],0
13FE81C17 jmp testSwitch+26h (13FE81C26h)
13FE81C19 mov rax,qword ptr [i]
13FE81C1E inc rax
13FE81C21 mov qword ptr [i],rax
13FE81C26 cmp qword ptr [i],20000000h
13FE81C2F jae testSwitch+0C5h (13FE81CC5h)
13FE81C35 xor edx,edx
13FE81C37 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C3E mov ecx,4
13FE81C43 div rax,rcx
13FE81C46 mov rax,rdx
13FE81C49 inc rax
13FE81C4C mov qword ptr [rsp+30h],rax
13FE81C51 cmp qword ptr [rsp+30h],1
13FE81C57 je testSwitch+73h (13FE81C73h)
13FE81C59 cmp qword ptr [rsp+30h],2
13FE81C5F je testSwitch+87h (13FE81C87h)
13FE81C61 cmp qword ptr [rsp+30h],3
13FE81C67 je testSwitch+9Bh (13FE81C9Bh)
13FE81C69 cmp qword ptr [rsp+30h],4
13FE81C6F je testSwitch+0AFh (13FE81CAFh)
13FE81C71 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C73 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C7A add rax,4
13FE81C7E mov qword ptr [counter (13FE835D0h)],rax
13FE81C85 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C87 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C8E add rax,3
13FE81C92 mov qword ptr [counter (13FE835D0h)],rax
13FE81C99 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C9B mov rax,qword ptr [counter (13FE835D0h)]
13FE81CA2 add rax,2
13FE81CA6 mov qword ptr [counter (13FE835D0h)],rax
13FE81CAD jmp testSwitch+0C0h (13FE81CC0h)
13FE81CAF mov rax,qword ptr [counter (13FE835D0h)]
13FE81CB6 inc rax
13FE81CB9 mov qword ptr [counter (13FE835D0h)],rax
13FE81CC0 jmp testSwitch+19h (13FE81C19h)
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)]
13FE81CCB sub eax,dword ptr [start]
13FE81CCF imul eax,eax,3E8h
13FE81CD5 cdq
13FE81CD6 mov ecx,3E8h
13FE81CDB idiv eax,ecx
13FE81CDD cdqe
13FE81CDF add rsp,48h
13FE81CE3 ret
Aktualisieren:
Interessante Ergebnisse hier . Ich bin mir nicht sicher, warum man schneller und langsamer ist.
c
performance
switch-statement
assembly
jump-table
user541686
quelle
quelle
5196 vs. 5261 shouldn't be enough to actually care
-> Ich bin mir nicht sicher, ob Sie die Frage falsch verstanden haben oder ob ich Ihren Kommentar falsch verstanden habe, aber ist es nicht der springende Punkt meiner Frage, zu fragen, warum es keinen Unterschied gibt? (Habe ich jemals behauptet, dass dies ein bedeutender Unterschied ist, um den man sich kümmern muss?)Antworten:
Es gibt verschiedene Optimierungen, die ein Compiler an einem Switch vornehmen kann . Ich denke nicht, dass die oft erwähnte "Sprungtabelle" sehr nützlich ist, da sie nur funktioniert, wenn die Eingabe auf irgendeine Weise begrenzt werden kann.
C Pseudocode für eine "Sprungtabelle" wäre ungefähr so - beachten Sie, dass der Compiler in der Praxis eine Art if-Test um die Tabelle einfügen müsste, um sicherzustellen, dass die Eingabe in der Tabelle gültig ist. Beachten Sie auch, dass dies nur in dem speziellen Fall funktioniert, in dem die Eingabe eine Folge von fortlaufenden Zahlen ist.
Wenn die Anzahl der Verzweigungen in einem Switch extrem groß ist, kann ein Compiler beispielsweise die binäre Suche nach den Werten des Switches durchführen, was (meiner Meinung nach) eine viel nützlichere Optimierung wäre, da dies in einigen Fällen die Leistung erheblich erhöht Szenarien ist so allgemein wie ein Switch und führt nicht zu einer größeren generierten Codegröße. Aber um das zu sehen, würde Ihr Testcode VIEL mehr Zweige benötigen, um einen Unterschied zu erkennen.
So beantworten Sie Ihre spezifischen Fragen:
Clang erzeugt man das sieht aus wie diese :
Ich kann sagen, dass keine Sprungtabelle verwendet wird - 4 Vergleichsanweisungen sind deutlich sichtbar:
Eine auf Sprungtabellen basierende Lösung verwendet überhaupt keinen Vergleich.
EDIT 2014 : An anderer Stelle gab es einige Diskussionen von Personen, die mit dem LLVM-Optimierer vertraut sind, dass die Optimierung der Sprungtabelle in vielen Szenarien wichtig sein kann. zB in Fällen, in denen es eine Aufzählung mit vielen Werten und viele Fälle gegen Werte in dieser Aufzählung gibt. Trotzdem stehe ich zu dem, was ich oben im Jahr 2011 gesagt habe - zu oft sehe ich Leute denken, "wenn ich es wechsle, wird es die gleiche Zeit sein, egal wie viele Fälle ich habe" - und das ist völlig falsch. Selbst mit einer Sprungtabelle erhalten Sie die indirekten Sprungkosten und zahlen für die Einträge in der Tabelle für jeden Fall; und Speicherbandbreite ist eine große Sache auf moderner Hardware.
Schreiben Sie Code zur besseren Lesbarkeit. Jeder Compiler, der sein Geld wert ist, wird eine if / else if-Leiter sehen und sie in einen äquivalenten Schalter umwandeln oder umgekehrt, wenn dies schneller wäre.
quelle
switch
Ausgänge. Soren hat einige andere Dinge gesagt, die ich sagen wollte, nachdem ich diese Antwort gelesen hatte.if
Klauseln bereits von Hand angepasst wurde, um der Häufigkeit und den relativen Leistungsanforderungen zu entsprechen. Diesswitch
wird traditionell als offene Aufforderung zur Optimierung angesehen, wie auch immer der Compiler dies wünscht. Guter Punkt, um vorbei zu springenswitch
:-). Die Codegröße hängt von den Fällen / dem Bereich ab - könnte besser sein. Schließlich sind einige Aufzählungen, Bitfelder undchar
Szenarien von Natur aus gültig / begrenzt und frei von Overhead.Zu Ihrer Frage:
1.Wie würde eine einfache Sprungtabelle in x86 oder x64 aussehen?
Die Sprungtabelle ist eine Speicheradresse, die einen Zeiger auf die Beschriftungen in einer Art Array-Struktur enthält. Das folgende Beispiel hilft Ihnen zu verstehen, wie Sprungtabellen angeordnet sind
Wobei 00B14538 der Zeiger auf die Sprungtabelle ist und ein Wert wie D8 09 AB 00 den Beschriftungszeiger darstellt.
2. Verwendet dieser Code eine Sprungtabelle? Nein in diesem Fall.
3. Warum gibt es in diesem Beispiel keinen Leistungsunterschied?
Es gibt keinen Leistungsunterschied, da die Anweisung für beide Fälle gleich aussieht, keine Sprungtabelle.
4. Gibt es eine Situation, in der es einen signifikanten Leistungsunterschied gibt?
Wenn Sie eine sehr lange Sequenz von if- Prüfungen haben, verbessert in diesem Fall die Verwendung einer Sprungtabelle die Leistung (Verzweigungs- / JPMP-Anweisungen sind teuer, wenn sie nicht nahezu perfekt vorhersagen), sind jedoch mit den Speicherkosten verbunden.
Der Code für alle Vergleichsanweisungen hat ebenfalls eine gewisse Größe. Insbesondere bei 32-Bit-Zeigern oder Offsets kostet eine einzelne Sprungtabellensuche in einer ausführbaren Datei möglicherweise nicht viel mehr Größe.
Fazit: Der Compiler ist klug genug, um einen solchen Fall zu behandeln und entsprechende Anweisungen zu generieren :)
quelle
gcc -S
Ausgabe einzuschließen : Eine Folge von.long L1
/.long L2
table-Einträgen ist aussagekräftiger als ein Hexdump und für jemanden, der dies nützlicher ist möchte lernen, wie man einen Compiler betrachtet. (Obwohl ich denke, Sie würden sich nur den Switch-Code ansehen, um zu sehen, ob es sich um einen indirekten JMP oder einen Haufen JCC handelt.)Dem Compiler steht es frei, die switch-Anweisung als Code zu kompilieren, der der if-Anweisung entspricht, oder eine Sprungtabelle zu erstellen. Es wird wahrscheinlich eine basierend auf der schnellsten Ausführung auswählen oder den kleinsten Code generieren, je nachdem, was Sie in Ihren Compileroptionen angegeben haben. Im schlimmsten Fall entspricht dies der Geschwindigkeit von if-Anweisungen
Ich würde darauf vertrauen, dass der Compiler die beste Wahl trifft und sich darauf konzentriert, was den Code am besten lesbar macht.
Wenn die Anzahl der Fälle sehr groß wird, ist eine Sprungtabelle viel schneller als eine Reihe von if. Wenn jedoch die Schritte zwischen den Werten sehr groß sind, kann die Sprungtabelle groß werden, und der Compiler kann sich dafür entscheiden, keine zu generieren.
quelle
Woher wissen Sie, dass Ihr Computer während der Switch-Testschleife keine Aufgabe ausgeführt hat, die nicht mit dem Test zusammenhängt, und während der if-Testschleife weniger Aufgaben ausgeführt hat? Ihre Testergebnisse zeigen nichts als:
Meine Ergebnisse:
Ich fügte hinzu:
bis zum Ende, damit die Schleife nicht optimiert wird, da in Ihrem Beispiel nie ein Zähler verwendet wurde. Warum sollte der Compiler die Schleife ausführen? Sofort gewann der Switch auch mit einem solchen Mikro-Benchmark immer.
Das andere Problem mit Ihrem Code ist:
in Ihrer Schaltschleife versus
in Ihrer if-Schleife. Sehr großer Unterschied, wenn Sie das beheben. Ich glaube, dass das Einfügen der Anweisung in die switch-Anweisung den Compiler dazu veranlasst, den Wert direkt in die CPU-Register zu senden, anstatt ihn zuerst auf den Stapel zu legen. Dies spricht daher für die switch-Anweisung und nicht für einen ausgeglichenen Test.
Oh und ich denke, Sie sollten auch den Zähler zwischen den Tests zurücksetzen. In der Tat sollten Sie wahrscheinlich eine Art Zufallszahl anstelle von +1, +2, +3 usw. verwenden, da dies dort wahrscheinlich etwas optimieren wird. Mit Zufallszahl meine ich beispielsweise eine Zahl, die auf der aktuellen Zeit basiert. Andernfalls könnte der Compiler beide Funktionen in eine lange mathematische Operation verwandeln und sich nicht einmal um Schleifen kümmern.
Ich habe Ryans Code gerade genug geändert, um sicherzustellen, dass der Compiler die Dinge nicht herausfinden konnte, bevor der Code ausgeführt wurde:
Schalter: 3740
wenn: 3980
(ähnliche Ergebnisse bei mehreren Versuchen)
Ich habe auch die Anzahl der Fälle / Wenns auf 5 reduziert und die Schaltfunktion hat immer noch gewonnen.
quelle
print
Aussage hinzugefügt ? Ich habe es am Ende des gesamten Programms hinzugefügt und keinen Unterschied festgestellt. Ich verstehe auch nicht, was das "Problem" mit dem anderen ist ... etwas dagegen zu erklären, was der "sehr große Unterschied" ist?Ein guter optimierender Compiler wie MSVC kann Folgendes generieren:
Kurz gesagt, wenn der Switch langsamer als eine Reihe von ifs zu sein scheint, konvertiert der Compiler ihn möglicherweise einfach in einen. Und es ist wahrscheinlich nicht nur eine Folge von Vergleichen für jeden Fall, sondern ein binärer Suchbaum. Siehe hier für ein Beispiel.
quelle
Ich werde 2) antworten und einige allgemeine Kommentare abgeben. 2) Nein, der von Ihnen veröffentlichte Assembler-Code enthält keine Sprungtabelle. Eine Sprungtabelle ist eine Tabelle mit Sprungzielen und eine oder zwei Anweisungen, um direkt von der Tabelle zu einer indizierten Position zu springen. Eine Sprungtabelle wäre sinnvoller, wenn es viele mögliche Switch-Ziele gibt. Vielleicht weiß der Optimierer, dass einfach, wenn sonst die Logik schneller ist, es sei denn, die Anzahl der Ziele ist größer als ein Schwellenwert. Versuchen Sie Ihr Beispiel noch einmal mit 20 statt 4 Möglichkeiten.
quelle
Ich war fasziniert und habe mir angesehen, was ich an Ihrem Beispiel ändern könnte, damit die switch-Anweisung schneller ausgeführt wird.
Wenn Sie 40 if-Anweisungen erhalten und einen 0-Fall hinzufügen, wird der if-Block langsamer ausgeführt als die entsprechende switch-Anweisung. Ich habe die Ergebnisse hier: https://www.ideone.com/KZeCz .
Die Auswirkung des Entfernens des 0-Falls ist hier zu sehen: https://www.ideone.com/LFnrX .
quelle
Hier sind einige Ergebnisse des alten (jetzt schwer zu findenden) Bench ++ Benchmarks:
Daraus können wir ersehen, dass (auf diesem Computer mit diesem Compiler - VC ++ 9.0 x64) jeder
if
Test etwa 0,7 Nanosekunden dauert. Mit steigender Anzahl von Tests skaliert die Zeit nahezu perfekt linear.Mit der switch-Anweisung gibt es fast keinen Geschwindigkeitsunterschied zwischen einem 2-Wege- und einem 10-Wege-Test, solange die Werte dicht sind. Der 10-Wege-Test mit spärlichen Werten dauert etwa 1,6-mal so lange wie der 10-Wege-Test mit dichten Werten - aber selbst bei spärlichen Werten immer noch besser als die doppelte Geschwindigkeit eines 10-Wege
if
/else if
.Fazit: Wenn Sie nur einen 4-Wege-Test verwenden, sehen Sie nicht viel über die Leistung von
switch
vsif
/else
. Wenn Sie sich die Zahlen aus diesem Code ansehen, ist es ziemlich einfach, die Tatsache zu interpolieren, dass wir für einen 4-Wege-Test erwarten würden, dass die beiden ziemlich ähnliche Ergebnisse liefern (~ 2,8 Nanosekunden für einif
/else
, ~ 2,0 fürswitch
).quelle
if
/else
Kette übereinstimmt, anstatt sie zu streuen usw. Diebench++
Quellen können nach 10 nicht gefunden werden Minuten googeln.Beachten Sie, dass Sie sehr oft schreiben können, wenn ein Switch NICHT zu einer Sprungtabelle kompiliert wird, wenn er effizienter ist als der Switch ...
(1) Wenn die Fälle eine Reihenfolge haben und nicht der Worst-Case-Test für alle N, können Sie Ihre Wenns schreiben, um zu testen, ob in der oberen oder unteren Hälfte, dann in jeder Hälfte davon, binärer Suchstil ... was zu Der schlimmste Fall ist logN statt N.
(2) Wenn bestimmte Fälle / Gruppen weitaus häufiger sind als andere Fälle, kann das Entwerfen Ihrer Wenns, um diese Fälle zuerst zu isolieren, die durchschnittliche Durchlaufzeit beschleunigen
quelle
Nein, diese sind, wenn dann springen, wenn dann springen, sonst ... Eine Sprungtabelle hätte eine Adressentabelle oder würde einen Hash oder ähnliches verwenden.
Schneller oder langsamer ist subjektiv. Sie könnten zum Beispiel Fall 1 als letztes statt als erstes haben, und wenn Ihr Testprogramm oder reales Programm Fall 1 meistens verwendet, wäre der Code bei dieser Implementierung langsamer. Das Neuanordnen der Fallliste in Abhängigkeit von der Implementierung kann also einen großen Unterschied machen.
Wenn Sie die Fälle 0-3 anstelle von 1-4 verwendet haben, hat der Compiler möglicherweise eine Sprungtabelle verwendet, und der Compiler hätte trotzdem herausfinden müssen, wie Sie Ihre +1 entfernen. Vielleicht war es die geringe Anzahl von Gegenständen. Wenn Sie es beispielsweise auf 0 - 15 oder 0 - 31 gesetzt haben, hat es es möglicherweise mit einer Tabelle implementiert oder eine andere Verknüpfung verwendet. Der Compiler kann frei wählen, wie er die Dinge implementiert, solange er die Funktionalität des Quellcodes erfüllt. Dies führt zu Compiler- und Versionsunterschieden sowie Optimierungsunterschieden. Wenn Sie eine Sprungtabelle möchten, erstellen Sie eine Sprungtabelle. Wenn Sie einen Wenn-Dann-Sonst-Baum möchten, erstellen Sie einen Wenn-Dann-Sonst-Baum. Wenn der Compiler entscheiden soll, verwenden Sie eine switch / case-Anweisung.
quelle
Das ist eigentlich nicht allzu schwer zu erklären ... Wenn Sie sich daran erinnern, dass falsch vorhergesagte Zweige zehn- bis hundertmal teurer sind als richtig vorhergesagte Zweige.
In dem
% 20
Version ist der erste Fall / if immer derjenige, der trifft. Moderne CPUs "lernen", welche Zweige normalerweise verwendet werden und welche nicht, sodass sie leicht vorhersagen können, wie sich dieser Zweig bei fast jeder Iteration der Schleife verhält. Das erklärt, warum die "wenn" -Version fliegt; Es muss nie etwas nach dem ersten Test ausführen und sagt das Ergebnis dieses Tests für die meisten Iterationen (korrekt) voraus. Offensichtlich ist der "Schalter" etwas anders implementiert - vielleicht sogar eine Sprungtabelle, die dank des berechneten Zweigs langsam sein kann.In dem
% 21
Version sind die Zweige im Wesentlichen zufällig. Viele von ihnen führen also nicht nur jede Iteration aus, die CPU kann auch nicht erraten, in welche Richtung sie gehen werden. Dies ist der Fall, wenn eine Sprungtabelle (oder eine andere "Schalter" -Optimierung) wahrscheinlich hilft.Es ist sehr schwer vorherzusagen, wie sich ein Code mit einem modernen Compiler und einer modernen CPU verhalten wird, und es wird mit jeder Generation schwieriger. Der beste Rat ist "nicht einmal die Mühe machen, es zu versuchen; immer Profil". Dieser Rat wird jedes Jahr besser - und die Anzahl der Leute, die ihn erfolgreich ignorieren können, wird kleiner.
All dies bedeutet, dass meine obige Erklärung größtenteils eine Vermutung ist. :-)
quelle
Keiner. In den meisten Fällen, in denen Sie in den Assembler gehen und echte Leistungsmessungen durchführen, ist Ihre Frage einfach die falsche. Für das gegebene Beispiel ist Ihr Denken seitdem definitiv zu kurz
scheint mir der richtige Inkrementausdruck zu sein, den Sie verwenden sollten.
quelle