Ist 'switch' schneller als 'if'?

242

Ist eine switchAussage tatsächlich schneller als eine ifAussage?

Ich habe den folgenden Code auf dem x64 C ++ - Compiler von Visual Studio 2010 mit dem folgenden /OxFlag 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 switchAnweisungen anscheinend Sprungtabellen, um die Verzweigung zu optimieren.

Fragen:

  1. Wie würde eine einfache Sprungtabelle in x86 oder x64 aussehen?

  2. Verwendet dieser Code eine Sprungtabelle?

  3. 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.

user541686
quelle
47
Was um alles in der Welt stimmen die Menschen dafür, dieses Denken zu schließen? Glauben sie so sehr an die Vorstellung des perfekt optimierenden Compilers, dass jeder Gedanke daran, dass er weniger als den idealen Code generiert, eine Häresie ist? Beleidigt sie die Idee einer Optimierung irgendwo ?
Crashworks
6
Was genau ist falsch an dieser Frage?
Tugrul Ates
25
Für jeden fragen , was mit dieser Frage falsch ist : Für den Anfang, es nicht ist eine Frage, ist es 3 Fragen, was bedeutet , dass viele der Antworten nun verschiedene Fragen. Dies bedeutet, dass es schwierig sein wird, eine Antwort zu akzeptieren, die alles beantwortet . Darüber hinaus besteht die typische Reaktion auf die obige Frage darin, sie als "nicht wirklich interessant" zu schließen, hauptsächlich aufgrund der Tatsache, dass Sie auf dieser Optimierungsstufe fast immer vorzeitig optimieren . Schließlich sollten 5196 vs. 5261 nicht ausreichen, um sich wirklich darum zu kümmern. Schreiben Sie den sinnvollen logischen Code.
Lasse V. Karlsen
40
@Lasse: Hättest du es wirklich vorgezogen, wenn ich stattdessen drei Fragen zu SO gestellt hätte? Außerdem: 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?)
user541686
5
@ Robert: Nun, es gibt nur mehr als 20 Kommentare, weil es sich um Metakommentare handelt. Es gibt hier nur 7 Kommentare zu dieser Frage. Meinung: Ich sehe nicht, wie es hier "Meinung" gibt. Es gibt einen Grund , warum ich keinen Leistungsunterschied sehe, nein? Ist es nur Geschmack? Debatte: Vielleicht, aber es sieht für mich nach einer gesunden Art von Debatte aus, wie ich sie an anderen Orten auf SO gesehen habe (lassen Sie mich wissen, ob etwas dagegen spricht). Argumente: Ich sehe hier nichts Argumentatives (es sei denn, Sie nehmen es als Synonym für "Debatte"?). Erweiterte Diskussion: Wenn Sie diese Metakommentare einfügen.
user541686

Antworten:

122

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:

  1. Clang erzeugt man das sieht aus wie diese :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
  2. Ich kann sagen, dass keine Sprungtabelle verwendet wird - 4 Vergleichsanweisungen sind deutlich sichtbar:

    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) 

    Eine auf Sprungtabellen basierende Lösung verwendet überhaupt keinen Vergleich.

  3. Entweder nicht genügend Zweige, um vom Compiler eine Sprungtabelle zu generieren, oder Ihr Compiler generiert sie einfach nicht. Ich bin mir nicht sicher welche.

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.

Billy ONeal
quelle
3
+1 für die tatsächliche Beantwortung der Frage und für nützliche Informationen. :-) Eine Frage: Soweit ich weiß, verwendet eine Sprungtabelle indirekte Sprünge. Ist das korrekt? Wenn ja, ist das normalerweise nicht langsamer, weil das Prefetching / Pipelining schwieriger ist?
user541686
1
@Mehrdad: Ja, es werden indirekte Sprünge verwendet. Ein indirekter Sprung (mit dem Pipeline-Stall, mit dem er geliefert wird) kann jedoch weniger als Hunderte von direkten Sprüngen betragen. :)
Billy ONeal
1
@Mehrdad: Nein, leider. :( Ich bin froh, dass ich im Lager der Leute bin, die immer denken, dass die IF besser lesbar ist! :)
Billy ONeal
1
Einige Witze - "[Schalter] funktionieren nur, wenn die Eingabe auf irgendeine Weise begrenzt werden kann" "müssen eine Form von if-Test um die Tabelle einfügen, um sicherzustellen, dass die Eingabe in der Tabelle gültig war. Beachten Sie auch, dass sie nur in der spezifischen Tabelle funktioniert Fall, dass die Eingabe eine Folge von fortlaufenden Zahlen ist. ": Es ist durchaus möglich, eine dünn besiedelte Tabelle zu haben, in der der potenzielle Zeiger gelesen wird, und nur, wenn nicht NULL ausgeführt wird, ein Sprung ausgeführt wird, andernfalls wird der Standardfall, zu dem gesprungen wird, dann die switchAusgänge. Soren hat einige andere Dinge gesagt, die ich sagen wollte, nachdem ich diese Antwort gelesen hatte.
Tony Delroy
2
"Jeder Compiler, der sein Geld wert ist, wird eine if / else if-Leiter sehen und sie in einen äquivalenten Schalter umwandeln oder umgekehrt" - irgendeine Unterstützung für diese Behauptung? Ein Compiler kann davon ausgehen, dass die Reihenfolge Ihrer ifKlauseln bereits von Hand angepasst wurde, um der Häufigkeit und den relativen Leistungsanforderungen zu entsprechen. Dies switchwird traditionell als offene Aufforderung zur Optimierung angesehen, wie auch immer der Compiler dies wünscht. Guter Punkt, um vorbei zu springen switch:-). Die Codegröße hängt von den Fällen / dem Bereich ab - könnte besser sein. Schließlich sind einige Aufzählungen, Bitfelder und charSzenarien von Natur aus gültig / begrenzt und frei von Overhead.
Tony Delroy
47

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

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

Geben Sie hier die Bildbeschreibung ein

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 :)

verschlüsselt
quelle
(edit: nvm, Billys Antwort hat bereits das, was ich vorgeschlagen habe. Ich denke, dies ist eine nette Ergänzung.) Es wäre gut, eine gcc -SAusgabe einzuschließen : Eine Folge von .long L1/ .long L2table-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.)
Peter Cordes
31

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.

Soren
quelle
13
Ich denke nicht, dass dies die Frage des OP beantwortet. Überhaupt.
Billy ONeal
5
@Soren: Wenn das die "Grundfrage" wäre, hätte ich mich nicht um die 179 anderen Zeilen in der Frage gekümmert, es wäre nur 1 Zeile gewesen. :-)
user541686
8
@Soren: Ich sehe mindestens 3 nummerierte Unterfragen als Teil der OP-Frage. Sie haben lediglich genau die gleiche Antwort gegeben, die für alle "Leistungs" -Fragen gilt - nämlich, dass Sie zuerst messen müssen. Bedenken Sie, dass Mehrdad möglicherweise bereits gemessen und diesen Code als Hot Spot isoliert hat. In solchen Fällen ist Ihre Antwort schlechter als wertlos, es ist Lärm.
Billy ONeal
2
Es gibt eine unscharfe Linie zwischen dem, was eine Sprungtabelle ist, und dem, was nicht von Ihrer Definition abhängt. Ich habe Informationen zu Unterfrage Teil 3 bereitgestellt.
Soren
2
@wnoise: Wenn es die einzig richtige Antwort ist, gibt es nie einen Grund, jemals eine Leistungsfrage zu stellen. Es gibt jedoch einige von uns in der realen Welt, die unsere Software messen, und manchmal wissen wir nicht, wie wir einen Code schneller machen können, wenn er einmal gemessen wurde. Es ist offensichtlich, dass Mehrdad sich einige Mühe gegeben hat, bevor er sie gestellt hat. und ich denke, seine spezifischen Fragen sind mehr als beantwortbar.
Billy ONeal
13

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:

  1. Der Unterschied ist sehr gering
  2. Es gibt nur ein Ergebnis, keine Reihe von Ergebnissen
  3. Es gibt zu wenige Fälle

Meine Ergebnisse:

Ich fügte hinzu:

printf("counter: %u\n", counter);

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:

switch (counter % 4 + 1)

in Ihrer Schaltschleife versus

const size_t c = counter % 4 + 1; 

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:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

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.

BobTurbo
quelle
Idk, ich kann es nicht beweisen; Erhalten Sie unterschiedliche Ergebnisse?
user541686
+1: Benchmarking ist schwierig, und Sie können wirklich keine Schlussfolgerungen aus einem kleinen Zeitunterschied bei einem einzelnen Lauf auf einem normalen Computer ziehen. Sie können versuchen, eine große Anzahl von Tests durchzuführen und Statistiken zu den Ergebnissen zu erstellen. Oder Zählen von Prozessorzyklen bei kontrollierter Ausführung in einem Emulator.
Thomas Padron-McCarthy
Äh, wo genau hast du die printAussage 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?
user541686
1
@ BobTurbo: 45983493 ist über 12 Stunden. War das ein Tippfehler?
Gus
1
toll, jetzt muss ich es nochmal machen :)
BobTurbo
7

Ein guter optimierender Compiler wie MSVC kann Folgendes generieren:

  1. eine einfache Sprungtabelle, wenn die Fälle in einer schönen großen Entfernung angeordnet sind
  2. eine spärliche (zweistufige) Sprungtabelle, wenn es viele Lücken gibt
  3. eine Reihe von Wenns, wenn die Anzahl der Fälle gering ist oder die Werte nicht nahe beieinander liegen
  4. eine Kombination von oben, wenn die Fälle mehrere Gruppen von eng beieinander liegenden Bereichen darstellen.

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.

Igor Skochinsky
quelle
Tatsächlich kann ein Compiler ihn auch durch einen Hash und einen Sprung ersetzen, was eine bessere Leistung als die von Ihnen vorgeschlagene spärliche zweistufige Lösung bietet.
Alice
5

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.

Bill Forster
quelle
+1 danke für die Antwort auf # 2! :) (Übrigens, hier sind die Ergebnisse mit mehr Möglichkeiten.)
user541686
4

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 .

Ryan Gross
quelle
1
Ihre Links sind zusammengebrochen.
TS
4

Hier sind einige Ergebnisse des alten (jetzt schwer zu findenden) Bench ++ Benchmarks:

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

Daraus können wir ersehen, dass (auf diesem Computer mit diesem Compiler - VC ++ 9.0 x64) jeder ifTest 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 switchvs if/ 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 ein if/ else, ~ 2,0 für switch).

Jerry Sarg
quelle
1
Es ist etwas schwierig zu wissen, was wir daraus machen sollen, wenn wir nicht wissen, ob der Test absichtlich einen Wert sucht, der nicht mit oder nur am Ende der if/ elseKette übereinstimmt, anstatt sie zu streuen usw. Die bench++Quellen können nach 10 nicht gefunden werden Minuten googeln.
Tony Delroy
3

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

Brian Kennedy
quelle
Dies ist ausgesprochen falsch; Compiler sind mehr als in der Lage, BEIDE dieser Optimierungen vorzunehmen.
Alice
1
Alice, woher soll ein Compiler wissen, welche Fälle in Ihren erwarteten Workloads häufiger auftreten als in anderen Fällen? (A: Es kann unmöglich wissen, also kann es unmöglich eine solche Optimierung durchführen.)
Brian Kennedy
(1) kann leicht durchgeführt werden und wird in einigen Compilern durch einfaches Ausführen einer binären Suche durchgeführt. (2) kann auf verschiedene Arten vorhergesagt oder dem Compiler angezeigt werden. Haben Sie noch nie "wahrscheinlich" oder "unwahrscheinlich" von GCC verwendet?
Alice
Einige Compiler ermöglichen es, das Programm in einem Modus auszuführen, in dem Statistiken erfasst und anschließend anhand dieser Informationen optimiert werden.
Phil 1970
2

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.

Oldtimer
quelle
2

Ich bin mir nicht sicher, warum man schneller und langsamer ist.

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. :-)

Nemo
quelle
2
Ich sehe nicht, woher hunderte Male langsamer kommen kann. Der schlimmste Fall eines falsch vorhergesagten Zweigs ist ein Pipeline-Stillstand, der auf den meisten modernen CPUs etwa 20-mal langsamer wäre. Nicht hunderte Male. (Okay, wenn Sie einen alten NetBurst-Chip verwenden, ist dieser möglicherweise 35-mal langsamer ...)
Billy ONeal
@ Billy: OK, also schaue ich ein wenig nach vorne. Auf Sandy Bridge-Prozessoren "spült jeder falsch vorhergesagte Zweig die gesamte Pipeline und verliert die Arbeit von bis zu etwa hundert Anweisungen während des Fluges". Die Pipelines werden mit jeder Generation im Allgemeinen wirklich tiefer ...
Nemo
1
Nicht wahr. Der P4 (NetBurst) hatte 31 Pipeline-Stufen; Sandy Bridge hat deutlich weniger Bühnen. Ich denke, der "Verlust der Arbeit von ungefähr 100 Anweisungen" geht davon aus, dass der Anweisungscache ungültig wird. Für einen allgemeinen indirekten Sprung, der tatsächlich passiert, aber für so etwas wie eine Sprungtabelle liegt das Ziel des indirekten Sprungs wahrscheinlich irgendwo im Anweisungscache.
Billy ONeal
@ Billy: Ich glaube nicht, dass wir nicht einverstanden sind. Meine Aussage war: "Falsch vorhergesagte Zweige sind zehn- bis hundertmal teurer als richtig vorhergesagte Zweige". Eine leichte Übertreibung vielleicht ... Aber es ist mehr los als nur Treffer in der Tiefe des I-Cache und der Ausführungspipeline; Nach dem, was ich gelesen habe, beträgt die Warteschlange für die Dekodierung allein ~ 20 Anweisungen.
Nemo
Wenn die Verzweigungsvorhersage-Hardware den Ausführungspfad falsch vorhersagt, werden die Uops aus dem falschen Pfad, die sich in der Anweisungspipeline befinden, einfach dort entfernt, wo sie sich befinden, ohne die Ausführung zu blockieren. Ich habe keine Ahnung, wie dies möglich ist (oder ob ich es falsch interpretiere), aber anscheinend gibt esin Nehalem keine Pipeline-Stände mitfalschvorhergesagten Zweigen? (
Andererseits
1

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

counter += (4 - counter % 4);

scheint mir der richtige Inkrementausdruck zu sein, den Sie verwenden sollten.

Jens Gustedt
quelle