'Switch' è più veloce di 'if'?


242

È una switchdichiarazione in realtà più veloce di un ifcomunicato?

Ho eseguito il codice seguente sul compilatore C ++ x64 di Visual Studio 2010 con il /Oxflag:

#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());
}

e ho ottenuto questi risultati:

Istruzione switch: 5261 ms
If istruzione: 5196 ms

Da quanto ho appreso, le switchdichiarazioni apparentemente usano le tabelle di salto per ottimizzare la ramificazione.

Domande:

  1. Come sarebbe una tabella di salto di base, in x86 o x64?

  2. Questo codice utilizza una tabella di salto?

  3. Perché non ci sono differenze di prestazioni in questo esempio? Esiste una situazione in cui vi è una differenza significativa di prestazioni?


Disassemblaggio del codice:

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       

Aggiornare:

Risultati interessanti qui . Non sono sicuro del motivo per cui uno è più veloce e uno è più lento, però.


47
Cosa diamine stanno votando le persone per chiudere questo pensiero? Sono tali credenti nella nozione di compilatore perfettamente ottimizzante che ogni pensiero di generare meno del codice ideale è eresia? L'idea stessa di qualche ottimizzazione ovunque li offende?
Crashworks,

6
Cosa c'è di sbagliato in questa domanda?
Tugrul Ates,

25
Per chiunque si chieda cosa c'è che non va in questa domanda : Per cominciare, non è una domanda, sono 3 domande, il che significa che molte delle risposte ora affrontano problemi diversi. Ciò significa che sarà difficile accettare qualsiasi risposta che risponda a tutto . Inoltre, la tipica reazione istintiva alla domanda di cui sopra è quella di chiuderlo come "non molto interessante" principalmente a causa del fatto che a questo livello di ottimizzazione, stai quasi sempre prematuramente ottimizzando . Infine, 5196 vs. 5261 non dovrebbero essere sufficienti per interessarsene. Scrivi il codice logico che ha senso.
Lasse V. Karlsen,

40
@Lasse: Mi avresti davvero preferito pubblicare tre domande su SO? Inoltre: 5196 vs. 5261 shouldn't be enough to actually care-> Non sono sicuro se hai frainteso la domanda o se ho frainteso il tuo commento, ma non è il punto centrale della mia domanda chiedermi perché non c'è differenza? (Ho mai affermato che è una differenza significativa di cui preoccuparsi?)
user541686,

5
@Robert: Beh, ha solo più di 20 commenti perché sono meta-commenti. Ci sono solo 7 commenti effettivamente correlati alla domanda qui. Opinione: Non vedo come ci sia "opinione" qui. C'è una ragione per cui non vedo differenze di prestazioni, no? È solo gusto? Dibattito: forse, ma a me sembra un sano tipo di dibattito, come ho visto in altri posti su SO (fammi sapere se c'è qualcosa di contrario). Argomenti: non vedo nulla di polemico qui (a meno che tu non lo stia prendendo come sinonimo di "dibattito"?). Discussione estesa: se includi questi meta-commenti.
user541686,

Risposte:


122

Esistono diverse ottimizzazioni che un compilatore può eseguire su uno switch. Non credo che la "tabella dei salti" spesso citata sia molto utile, poiché funziona solo quando l'input può essere limitato in qualche modo.

C Lo pseudocodice per una "tabella di salto" sarebbe simile a questo - si noti che il compilatore in pratica avrebbe bisogno di inserire una forma di if test intorno alla tabella per assicurarsi che l'input fosse valido nella tabella. Si noti inoltre che funziona solo nel caso specifico che l'input sia una sequenza di numeri consecutivi.

Se il numero di rami in uno switch è estremamente elevato, un compilatore può fare cose come usare la ricerca binaria sui valori dello switch, che (a mio avviso) sarebbe un'ottimizzazione molto più utile, poiché aumenta significativamente le prestazioni in alcuni scenari, è generale come un interruttore e non comporta una maggiore dimensione del codice generato. Ma per vederlo, il tuo codice di test avrebbe bisogno di MOLTE più filiali per vedere qualsiasi differenza.

Per rispondere a domande specifiche:

  1. Clang genera uno che assomiglia a questo :

    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. Posso dire che non sta usando una tabella di salto - 4 istruzioni di confronto sono chiaramente visibili:

    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) 
    

    Una soluzione basata su una tabella di salto non utilizza affatto il confronto.

  3. Non ci sono abbastanza rami per far sì che il compilatore generi una tabella di salto, o semplicemente il compilatore non li genera. Non sono sicuro di quale.

EDIT 2014 : c'è stata qualche discussione altrove da parte di persone che hanno familiarità con l'ottimizzatore LLVM affermando che l'ottimizzazione della tabella di salto può essere importante in molti scenari; ad es. nei casi in cui vi sia un'enumerazione con molti valori e molti casi contro valori in detta enumerazione. Detto questo, sostengo ciò che ho detto sopra nel 2011 - troppo spesso vedo la gente pensare "se lo cambio, sarà lo stesso tempo, non importa quanti casi ho" - ed è completamente falso. Anche con una tabella di salto si ottiene il costo di salto indiretto e si paga per le voci nella tabella per ogni caso; e la larghezza di banda della memoria è un grosso problema per l'hardware moderno.

Scrivi il codice per leggibilità. Qualsiasi compilatore degno di nota vedrà un ladder if / else e lo trasformerà in un interruttore equivalente o viceversa se sarebbe più veloce farlo.


3
+1 per aver effettivamente risposto alla domanda e per informazioni utili. :-) Tuttavia, una domanda: da quello che ho capito, una tabella di salto utilizza salti indiretti ; è corretto? In tal caso, di solito non è più lento a causa del prefetching / pipeline più difficile?
user541686

1
@Mehrdad: Sì, utilizza salti indiretti. Tuttavia, un salto indiretto (con lo stallo della pipeline in dotazione) può essere inferiore a centinaia di salti diretti. :)
Billy ONeal,

1
@Mehrdad: No, sfortunatamente. :( Sono contento di essere nel campo di persone che pensano sempre che l'IF sia più leggibile! :)
Billy ONeal

1
Poche battute - "[switch] funziona solo quando l'input può essere limitato in qualche modo" "è necessario inserire una forma di if test attorno alla tabella per assicurarsi che l'input sia valido nella tabella. Si noti inoltre che funziona solo nello specifico nel caso in cui l'input sia una sequenza di numeri consecutivi. ": è del tutto possibile avere una tabella scarsamente popolata, in cui viene letto il potenziale puntatore e solo se non-NULL viene eseguito un salto, altrimenti il ​​caso predefinito se ne viene eseguito uno, quindi le switchuscite. Soren ha detto diverse altre cose che volevo dire dopo aver letto questa risposta.
Tony Delroy,

2
"Qualsiasi compilatore degno di nota vedrà una ladder if / else if e la trasformerà in un interruttore equivalente o viceversa" - qualche supporto per questa affermazione? un compilatore potrebbe presumere che l'ordine delle tue ifclausole sia già stato regolato a mano per soddisfare le esigenze di frequenza e prestazioni relative, laddove switchtradizionalmente viene visto come un invito aperto a ottimizzare, comunque il compilatore lo scelga. Buon punto per saltare oltre switch:-). La dimensione del codice dipende da casi / intervallo: potrebbe essere migliore. Infine, alcuni enumerati, campi di bit e charscenari sono intrinsecamente validi / limitati e liberi da spese generali.
Tony Delroy,

47

Alla tua domanda:

1.Che aspetto avrebbe una tabella di salto di base, in x86 o x64?

La tabella di salto è un indirizzo di memoria che mantiene il puntatore alle etichette in qualcosa di simile alla struttura dell'array. l'esempio seguente ti aiuterà a capire come sono disposte le tabelle di salto

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

inserisci qui la descrizione dell'immagine

Dove 00B14538 è il puntatore alla tabella Jump e un valore come D8 09 AB 00 rappresenta il puntatore dell'etichetta.

2. Questo codice utilizza una tabella di salto? No in questo caso.

3.Perché non ci sono differenze di prestazioni in questo esempio?

Non ci sono differenze di prestazioni perché le istruzioni per entrambi i casi sembrano uguali, nessuna tabella di salto.

4. Esiste una situazione in cui vi è una differenza significativa di prestazioni?

Se hai una sequenza molto lunga di if check, in quel caso l'uso di una tabella di salto migliora le prestazioni (le istruzioni di ramificazione / jmp sono costose se non prevedono quasi alla perfezione) ma hanno il costo della memoria.

Il codice per tutte le istruzioni di confronto ha anche delle dimensioni, quindi specialmente con puntatori o offset a 32 bit, una singola ricerca della tabella di salto potrebbe non costare molto più dimensioni in un eseguibile.

Conclusione: il compilatore è abbastanza intelligente da gestire questo caso e generare istruzioni appropriate :)


(modifica: nvm, la risposta di Billy ha già quello che stavo suggerendo. Immagino che questo sia un bel complemento.) Sarebbe bene includere l' gcc -Soutput: una sequenza di voci .long L1/ .long L2table è più significativa di un hexdump e più utile per qualcuno che vuole imparare a guardare il compilatore. (Anche se immagino che guarderesti semplicemente il codice switch per vedere se fosse un jmp indiretto o un gruppo di jcc).
Peter Cordes,

31

Il compilatore è libero di compilare l'istruzione switch come un codice equivalente a if-statement o di creare una tabella di salto. Probabilmente sceglierà uno sull'altro in base a ciò che verrà eseguito più velocemente o genererà il codice più piccolo in qualche modo a seconda di ciò che hai specificato nelle opzioni del compilatore, quindi nel caso peggiore sarà la stessa velocità delle istruzioni if

Mi affiderei al compilatore per fare la scelta migliore e concentrarmi su ciò che rende il codice più leggibile.

Se il numero di casi diventa molto grande, una tabella di salto sarà molto più veloce di una serie di if. Tuttavia, se i passaggi tra i valori sono molto grandi, la tabella di salto può diventare grande e il compilatore può scegliere di non generarne uno.


13
Non credo che questo risponda alla domanda del PO. Affatto.
Billy ONeal,

5
@Soren: Se questa fosse la "domanda di base", non mi sarei disturbato con le altre 179 righe della domanda, sarebbe stata solo 1 riga. :-)
user541686

8
@Soren: vedo almeno 3 domande secondarie numerate come parte della domanda del PO. Hai semplicemente trombato la stessa identica risposta che si applica a tutte le domande sulla "performance" - vale a dire, che devi misurare prima. Considera che forse Mehrdad ha già misurato e ha isolato questo pezzo di codice come hot spot. In questi casi, la tua risposta è peggio che inutile, è rumore.
Billy ONeal,

2
C'è una linea sfocata tra ciò che è una tabella di salto e ciò che non dipende dalla tua definizione. Ho fornito informazioni sulla sottointerrogazione parte 3.
Soren,

2
@wnoise: se è l'unica risposta giusta, non ci sarebbe mai un motivo per porre mai una domanda sulla performance. Tuttavia, ci sono alcuni di noi nel mondo reale che misurano il nostro software e a volte non sappiamo come rendere più veloce un pezzo di codice una volta che è stato misurato. È ovvio che Mehrdad abbia fatto qualche sforzo in questa domanda prima di porla; e penso che le sue domande specifiche siano più che rispondenti.
Billy ONeal,

13

Come fai a sapere che il tuo computer non stava eseguendo alcune attività non correlate al test durante il ciclo di test switch e eseguendo meno attività durante il ciclo if test? I risultati del test non mostrano nulla come:

  1. la differenza è molto piccola
  2. c'è solo un risultato, non una serie di risultati
  3. ci sono troppi casi

I miei risultati:

Ho aggiunto:

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

fino alla fine in modo che non ottimizzasse il ciclo poiché il contatore non è mai stato usato nel tuo esempio, quindi perché il compilatore dovrebbe eseguire il ciclo? Immediatamente, l'interruttore ha sempre vinto anche con un tale micro-benchmark.

L'altro problema con il tuo codice è:

switch (counter % 4 + 1)

nel tuo ciclo di commutazione, contro

const size_t c = counter % 4 + 1; 

nel tuo ciclo if. Una grande differenza se la risolvi. Credo che l'inserimento dell'istruzione all'interno dell'istruzione switch provochi il compilatore a inviare il valore direttamente nei registri della CPU anziché metterlo per primo nello stack. Questo è quindi a favore dell'istruzione switch e non di un test bilanciato.

Oh e penso che dovresti anche resettare il contatore tra i test. In effetti, probabilmente dovresti usare una sorta di numero casuale invece di +1, +2, +3 ecc., Poiché probabilmente ottimizzerà qualcosa lì. Per numero casuale, intendo un numero basato sull'ora corrente, ad esempio. Altrimenti, il compilatore potrebbe trasformare entrambe le funzioni in una lunga operazione matematica e nemmeno disturbarsi con alcun loop.

Ho modificato il codice di Ryan quanto bastava per assicurarmi che il compilatore non potesse capire le cose prima che il codice fosse eseguito:

#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);
} 

interruttore: 3740
se: 3980

(risultati simili su più tentativi)

Ho anche ridotto il numero di casi / if a 5 e la funzione switch è stata ancora vinta.


Idk, non posso provarlo; ottieni risultati diversi?
user541686

+1: Il benchmarking è difficile e non puoi davvero trarre conclusioni da una piccola differenza di tempo su una singola corsa su un normale computer. Puoi provare a eseguire un gran numero di test e fare alcune statistiche sui risultati. O contando i cicli del processore sull'esecuzione controllata in un emulatore.
Thomas Padron-McCarthy,

Ehm, dove hai aggiunto esattamente la printdichiarazione? L'ho aggiunto alla fine dell'intero programma e non ho visto alcuna differenza. Inoltre non capisco quale sia il "problema" con l'altro ... ti dispiace spiegare qual è la "grande differenza"?
user541686

1
@BobTurbo: 45983493 è di oltre 12 ore. Era un errore di battitura?
Gus,

1
bene, ora devo andare a farlo di nuovo :)
BobTurbo il

7

Un buon compilatore di ottimizzazione come MSVC può generare:

  1. un semplice tavolo da salto se le custodie sono disposte in un lungo intervallo
  2. una tabella di salto sparsa (a due livelli) se ci sono molte lacune
  3. una serie di if se il numero di casi è piccolo o se i valori non sono vicini tra loro
  4. una combinazione di cui sopra se i casi rappresentano diversi gruppi di intervalli ravvicinati.

In breve, se l'interruttore sembra essere più lento di una serie di if, il compilatore potrebbe semplicemente convertirlo in uno. Ed è probabile che non sia solo una sequenza di confronti per ciascun caso, ma un albero di ricerca binario. Vedi qui per un esempio.


In realtà, un compilatore è anche in grado di sostituirlo con un hash e un salto, che offre prestazioni migliori rispetto alla soluzione sparsa a due livelli che proponi.
Alice,

5

Risponderò 2) e farò alcuni commenti generali. 2) No, non è presente alcuna tabella di salto nel codice assembly che hai pubblicato. Una tabella di salto è una tabella di destinazioni di salto e una o due istruzioni per passare direttamente a una posizione indicizzata dalla tabella. Una tabella di salto avrebbe più senso quando ci sono molte possibili destinazioni di commutazione. Forse l'ottimizzatore sa che la logica semplice se altrimenti è più veloce a meno che il numero di destinazioni sia maggiore di qualche soglia. Riprova il tuo esempio dicendo 20 possibilità invece di 4.


+1 grazie per la risposta al n. 2! :) (A proposito, ecco i risultati con maggiori possibilità.)
user541686

4

Ero incuriosito e ho dato un'occhiata a cosa avrei potuto cambiare del tuo esempio per farlo funzionare più velocemente.

Se si arriva a 40 istruzioni if ​​e si aggiunge un caso 0, il blocco if verrà eseguito più lentamente dell'istruzione switch equivalente. Ho i risultati qui: https://www.ideone.com/KZeCz .

L'effetto della rimozione del caso 0 può essere visto qui: https://www.ideone.com/LFnrX .


1
I tuoi collegamenti sono stati suddivisi.
TS,

4

Ecco alcuni risultati del vecchio benchmark Benchmark (ora difficile da trovare):

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

Ciò che possiamo vedere da questo è che (su questa macchina, con questo compilatore - VC ++ 9.0 x64), ogni iftest richiede circa 0,7 nanosecondi. Man mano che il numero di test aumenta, il tempo si ridimensiona in modo quasi perfettamente lineare.

Con l'istruzione switch, non c'è quasi alcuna differenza di velocità tra un test a 2 e uno a 10, purché i valori siano densi. Il test a 10 vie con valori sparsi richiede circa 1,6 volte più tempo del test a 10 vie con valori densi - ma anche con valori sparsi, comunque migliore del doppio della velocità di un 10 vie if/ else if.

In conclusione: usare solo un test a 4 vie non ti mostrerà molto sulle prestazioni di switchvs if/ else. Se guardi i numeri da questo codice, è abbastanza facile interpolare il fatto che per un test a 4 vie, ci aspetteremmo che i due producano risultati abbastanza simili (~ 2,8 nanosecondi per un if/ else, ~ 2,0 per switch).


1
È un po 'difficile sapere cosa pensare se non sappiamo se il test cerca deliberatamente un valore non abbinato o solo abbinato alla fine della catena if/ elserispetto alla loro dispersione ecc. Non riesci a trovare le bench++fonti dopo 10 minuti su Google.
Tony Delroy,

3

Nota che quando uno switch NON è compilato in una tabella di salto, puoi spesso scrivere se è più efficiente dello switch ...

(1) se i casi hanno un ordinamento, anziché il test del caso peggiore per tutte le N, puoi scrivere i tuoi if per verificare se nella metà superiore o inferiore, quindi in ogni metà di quello, lo stile di ricerca binario ... il caso peggiore è logN anziché N

(2) se alcuni casi / gruppi sono molto più frequenti rispetto ad altri casi, quindi progettare i tuoi if per isolarli prima può accelerare il tempo medio attraverso


Questo è marcatamente falso; i compilatori sono più che in grado di fare ENTRAMBE queste ottimizzazioni.
Alice,

1
Alice, come può un compilatore sapere quali casi si verificheranno più comunemente degli altri casi nei carichi di lavoro previsti? (A: Non può assolutamente saperlo, quindi non può assolutamente fare una tale ottimizzazione.)
Brian Kennedy,

(1) può essere fatto facilmente, ed è fatto in alcuni compilatori, semplicemente facendo una ricerca binaria. (2) può essere previsto in vari modi o indicato al compilatore. Non hai mai usato "probabile" o "improbabile" di GCC?
Alice,

E alcuni compilatori consentono di eseguire il programma in una modalità che raccoglie le statistiche e quindi ottimizza da tali informazioni.
Phil1970,

2

No, questi sono se poi salta altro se poi salta altro ... Una tabella di salto avrebbe una tabella di indirizzi o userebbe un hash o qualcosa del genere.

Più veloce o più lento è soggettivo. Ad esempio, potresti avere il caso 1 come ultima cosa anziché prima e se il tuo programma di test o il programma del mondo reale utilizzavano il caso 1 il più delle volte il codice sarebbe più lento con questa implementazione. Quindi, semplicemente riorganizzare l'elenco dei casi, a seconda dell'implementazione, può fare una grande differenza.

Se avessi usato i casi 0-3 invece di 1-4, il compilatore avrebbe potuto usare una tabella di salto, il compilatore avrebbe dovuto capire rimuovendo comunque il tuo +1. Forse era il piccolo numero di articoli. Se lo avessi fatto 0 - 15 o 0 - 31 per esempio, potrebbe averlo implementato con una tabella o usato qualche altra scorciatoia. Il compilatore è libero di scegliere come implementare le cose purché soddisfi le funzionalità del codice sorgente. E questo si traduce in differenze del compilatore, differenze di versione e differenze di ottimizzazione. Se vuoi una tabella di salto, crea una tabella di salto, se vuoi un albero if-then-else crea un albero if-then-else. Se vuoi che il compilatore decida, usa un'istruzione switch / case.


2

Non sono sicuro del motivo per cui uno è più veloce e uno è più lento, però.

Questo non è in realtà troppo difficile da spiegare ... Se ricordi che i rami mal previsti sono decine a centinaia di volte più costosi dei rami previsti correttamente.

Nella % 20versione, il primo caso / if è sempre quello che colpisce. Le CPU moderne "apprendono" quali rami sono normalmente presi e quali no, quindi possono facilmente prevedere come questo ramo si comporterà su quasi ogni iterazione del ciclo. Questo spiega perché la versione "if" vola; non deve mai eseguire nulla oltre il primo test e (correttamente) prevede il risultato di quel test per la maggior parte delle iterazioni. Ovviamente lo "switch" è implementato in modo leggermente diverso - forse anche una tabella di salto, che può essere lenta grazie al ramo calcolato.

Nella % 21versione, i rami sono essenzialmente casuali. Quindi non solo molti di loro eseguono ogni iterazione, ma la CPU non può indovinare da che parte andranno. Questo è il caso in cui una tabella di salto (o altra ottimizzazione "switch") è probabilmente utile.

È molto difficile prevedere come funzionerà un pezzo di codice con un compilatore e una CPU moderni, e diventa più difficile con ogni generazione. Il miglior consiglio è "non preoccuparti nemmeno di provare; profila sempre". Quel consiglio migliora - e l'insieme di persone che possono ignorarlo con successo si riduce - ogni anno.

Tutto ciò per dire che la mia spiegazione sopra è in gran parte un'ipotesi. :-)


2
Non vedo da dove possano venire centinaia di volte più lente. Il caso peggiore di una diramazione non prevista è uno stallo della pipeline, che sarebbe circa 20 volte più lento sulla maggior parte delle CPU moderne. Non centinaia di volte. (Va bene, se stai usando un vecchio chip NetBurst potrebbe essere 35 volte più lento ...)
Billy ONeal

@Billy: OK, quindi sto guardando un po 'avanti. Sui processori Sandy Bridge , "Ogni ramo mal previsto scaricherà l'intero gasdotto, perdendo il lavoro di un centinaio di istruzioni in volo". Le condutture diventano davvero più profonde con ogni generazione, in generale ...
Nemo il

1
Non vero. Il P4 (NetBurst) aveva 31 stadi della pipeline; Sandy Bridge ha molte meno fasi. Penso che "perdere il lavoro di circa 100 istruzioni" sia presupposto che la cache delle istruzioni venga invalidata. Per un salto indiretto generale che in effetti accade, ma per qualcosa come una tabella di salto è probabile che l'obiettivo del salto indiretto si trovi da qualche parte nella cache delle istruzioni.
Billy ONeal,

@Billy: non credo che non siamo d'accordo. La mia affermazione era: "I rami erroneamente sono decine a centinaia di volte più costosi dei rami previsti correttamente". Una leggera esagerazione, forse ... Ma c'è di più in corso oltre ai semplici hit nella cache dell'I e nella profondità della pipeline di esecuzione; da quello che ho letto, la coda per la decodifica da sola è di ~ 20 istruzioni.
Nemo,

Se l'hardware di previsione della diramazione indica in modo errato il percorso di esecuzione, gli uops dal percorso errato che si trovano nella pipeline delle istruzioni vengono semplicemente rimossi dove si trovano, senza arrestare l'esecuzione. Non ho idea di come questo sia possibile (o se sto interpretando male), ma a quanto pare non ci sono nessun bancarelle di pipeline con filiali pronosticate male a Nehalem? (Ancora una volta, non ho un i7; ho un i5, quindi questo non si applica al mio caso.)
user541686

1

Nessuna. Nei casi più particolari in cui si va nell'assemblatore e si eseguono misurazioni reali delle prestazioni, la domanda è semplicemente sbagliata. Per l'esempio dato, il tuo pensiero diventa definitivamente troppo breve da allora

counter += (4 - counter % 4);

mi sembra l'espressione di incremento corretta che dovresti usare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.