web-dev-qa-db-fra.com

Est-ce que 'switch' est plus rapide que 'if'?

Une instruction switch est-elle réellement plus rapide qu'une instruction if?

J'ai exécuté le code ci-dessous sur le compilateur x64 C++ de Visual Studio 2010 avec le /Ox drapeau:

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

et obtenu ces résultats:

Instruction de commutation: 5261 ms
Si déclaration: 5196 ms

D'après ce que j'ai appris, les instructions switch utilisent apparemment des tables de sauts pour optimiser le branchement.

Des questions:

  1. À quoi ressemblerait une table de saut de base, en x86 ou x64?

  2. Ce code utilise-t-il une table de saut?

  3. Pourquoi n'y a-t-il pas de différence de performance dans cet exemple? Existe-t-il une situation où une différence de performance significative ?


Démontage du code:

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       

Mise à jour:

Résultats intéressants ici . Pas sûr pourquoi on est plus rapide et on est plus lent, cependant.

232
Mehrdad

Il existe plusieurs optimisations qu'un compilateur peut effectuer sur un commutateur. Je ne pense pas que la "table de saut" souvent mentionnée soit très utile, car elle ne fonctionne que lorsque l'entrée peut être limitée.

C Le pseudocode pour une "table de saut" serait quelque chose comme this - notez que le compilateur aurait en pratique besoin d'insérer une forme de si test autour de la table pour s'assurer que l'entrée était valide dans la table . Notez également que cela ne fonctionne que dans le cas spécifique où l'entrée est une suite de nombres consécutifs.

Si le nombre de branches dans un commutateur est extrêmement important, un compilateur peut faire des choses comme utiliser une recherche binaire sur les valeurs du commutateur, ce qui (dans mon esprit) serait une optimisation beaucoup plus utile, dans la mesure où il améliore considérablement les performances dans certains cas. scénarios, est aussi générale qu’un commutateur, et n’entraîne pas une plus grande taille de code généré. Mais pour voir cela, votre code de test aurait besoin de BEAUCOUP plus de branches pour voir la différence.

Pour répondre à vos questions spécifiques:

  1. Clang en génère un qui ressemble à this :

    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. Je peux dire qu'il n'utilise pas de table de saut - 4 instructions de comparaison sont clairement visibles:

    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) 
    

    Une solution basée sur une table de saut n’utilise pas du tout la comparaison.

  3. Soit pas assez de branches pour que le compilateur génère une table de sauts, soit votre compilateur ne les génère tout simplement pas. Je ne sais pas lequel.

EDIT 2014 : Des personnes familiarisées avec l'optimiseur LLVM ont également expliqué que l'optimisation de la table de saut pouvait être importante dans de nombreux scénarios. par exemple. dans les cas où il y a une énumération avec beaucoup de valeurs et de nombreux cas contre des valeurs dans ladite énumération. Cela dit, je m'en tiens à ce que j'ai dit plus haut en 2011 - trop souvent, je vois des gens qui pensent "si je permets un changement, ce sera la même heure, peu importe le nombre de cas que je connaisse" - et c'est complètement faux. Même avec une table de saut, vous obtenez le coût du saut indirect et vous payez les entrées dans la table pour chaque cas; et la bande passante mémoire est un gros problème sur le matériel moderne.

Écrivez le code pour la lisibilité. Tout compilateur digne de ce nom va voir un if/else si Ladder et le transformer en commutateur équivalent ou vice versa s'il serait plus rapide de le faire.

117
Billy ONeal

A votre question:

1. À quoi ressemblerait une table de sauts de base, en x86 ou x64?

La table de saut est une adresse mémoire qui contient un pointeur sur les étiquettes dans une structure semblable à celle d'un tableau. l'exemple suivant vous aidera à comprendre comment sont disposées les tables de saut

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

enter image description here

00B14538 est le pointeur sur la table de sauts et une valeur semblable à D8 09 AB 00 représente le pointeur de l'étiquette.

2. Ce code utilise-t-il une table de saut? Non dans ce cas.

3.Pourquoi n'y a-t-il pas de différence de performance dans cet exemple?

Il n'y a pas de différence de performance car les instructions pour les deux cas ont la même apparence, pas de table de saut.

4.Y at-il une situation dans laquelle il existe une différence de performance significative?

Si vous avez une très longue séquence de si check , dans ce cas, l'utilisation d'une table de saut améliore les performances (les instructions de branchement/jmp sont cher s’ils ne prédisent pas de façon quasi parfaite) mais vient avec le coût de la mémoire.

Le code de toutes les instructions de comparaison a également une certaine taille. Par conséquent, en particulier avec les pointeurs ou les décalages 32 bits, une simple recherche dans une table de saut peut ne pas coûter beaucoup plus en taille dans un exécutable.

Conclusion: le compilateur est assez intelligent pour gérer ce cas et générer les instructions appropriées :)

43
crypted

Le compilateur est libre de compiler l'instruction switch en tant que code équivalent à if-statement ou de créer une table de saut. Il choisira probablement l’un sur l’autre en fonction de ce qui sera exécuté le plus rapidement ou générera le code le plus petit en fonction de ce que vous avez spécifié dans les options du compilateur.

Je ferais confiance au compilateur pour faire le meilleur choix et me concentrer sur ce qui rend le code le plus lisible.

Si le nombre de cas devient très grand, une table de saut sera beaucoup plus rapide qu'une série de if. Cependant, si les pas entre les valeurs sont très grands, la table de sauts peut devenir grande et le compilateur peut choisir de ne pas en générer.

31
Soren

Comment savez-vous que votre ordinateur n'exécutait pas une tâche non liée au test pendant la boucle de test du commutateur et effectuait moins de tâches au cours de la boucle if? Vos résultats de test ne montrent rien comme:

  1. la différence est très petite
  2. il n'y a qu'un seul résultat, pas une série de résultats
  3. il y a trop peu de cas

Mes résultats:

J'ai ajouté:

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

à la fin pour ne pas optimiser la boucle car le compteur n’a jamais été utilisé dans votre exemple, alors pourquoi le compilateur effectuerait-il la boucle? Immédiatement, le changement a toujours été gagnant, même avec un tel micro-benchmark.

L'autre problème avec votre code est:

switch (counter % 4 + 1)

dans votre boucle de commutation, par rapport à

const size_t c = counter % 4 + 1; 

dans votre boucle if. Très grande différence si vous corrigez cela. Je crois que l'insertion de l'instruction dans l'instruction switch provoque le compilateur à envoyer la valeur directement dans les registres de la CPU plutôt que de la placer d'abord sur la pile. Ceci est donc en faveur de la déclaration de commutateur et non d'un test équilibré.

Oh et je pense que vous devriez également réinitialiser le compteur entre les tests. En fait, vous devriez probablement utiliser une sorte de nombre aléatoire au lieu de +1, +2, +3, etc., car cela optimisera probablement quelque chose. Par nombre aléatoire, j'entends par exemple un nombre basé sur l'heure actuelle. Sinon, le compilateur pourrait transformer vos deux fonctions en une opération mathématique longue sans même vous embêter avec les boucles.

J'ai modifié le code de Ryan juste assez pour m'assurer que le compilateur ne pourrait pas comprendre les choses avant que le code ne soit exécuté:

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

commutateur: 3740
si: 3980

(résultats similaires sur plusieurs tentatives)

J'ai également réduit le nombre de cas/ifs à 5 et la fonction de commutation est toujours gagnante.

13
BobTurbo

Un compilateur optimiseur tel que MSVC peut générer:

  1. une simple table de saut si les étuis sont rangés dans une longue portée de Nice
  2. une table de sauts peu dense (à deux niveaux) s'il y a beaucoup de lacunes
  3. une série de ifs si le nombre de cas est faible ou si les valeurs ne sont pas rapprochées
  4. une combinaison de ce qui précède si les cas représentent plusieurs groupes de plages très rapprochées.

En bref, si le commutateur semble être plus lent qu'une série de if, le compilateur pourrait simplement le convertir en un. Et ce ne sera probablement pas simplement une séquence de comparaisons pour chaque cas, mais un arbre de recherche binaire. Voir ici pour un exemple.

7
Igor Skochinsky

Je vais répondre 2) et faire quelques commentaires généraux. 2) Non, il n'y a pas de table de raccourci dans le code d'assemblage que vous avez posté. Une table de saut est une table de destinations de saut et une ou deux instructions pour sauter directement à un emplacement indexé à partir de la table. Une table de saut aurait plus de sens lorsqu'il existe plusieurs destinations de commutation possibles. Peut-être que l’optimiseur le sait si simple, sinon la logique est plus rapide, sauf si le nombre de destinations est supérieur à un seuil. Recommencez votre exemple avec 20 possibilités au lieu de 4.

5
Bill Forster

J'étais intriguée et j'ai examiné ce que je pouvais changer à propos de votre exemple pour qu'il puisse exécuter l'instruction switch plus rapidement.

Si vous obtenez 40 instructions if et ajoutez une casse 0, le bloc if s'exécutera plus lentement que l'instruction switch équivalente. J'ai les résultats ici: https://www.ideone.com/KZeCz .

L'effet de supprimer le cas 0 peut être vu ici: https://www.ideone.com/LFnrX .

4
Ryan Gross

Voici quelques résultats de l'ancien benchmark bench ++ (maintenant difficile à trouver):

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

Ce que nous pouvons voir de ceci est que (sur cette machine, avec ce compilateur - VC++ 9.0 x64), chaque test if prend environ 0,7 nanosecondes. Au fur et à mesure que le nombre de tests augmente, le temps est presque parfaitement linéaire.

Avec l'instruction switch, il n'y a presque pas de différence de vitesse entre un test à 2 et 10 tests, tant que les valeurs sont denses. Le test à 10 voies avec des valeurs clairsemées prend environ 1,6 fois plus de temps que le test à 10 voies avec des valeurs denses - mais même avec des valeurs clairsemées, il est toujours supérieur au double de la vitesse d'un processus à 10 voies if/else if.

En bout de ligne: utiliser seulement un test à 4 voies ne vous montrera pas vraiment beaucoup sur les performances de switch vs if/else. Si vous regardez les chiffres de ce code, il est assez facile d'interpoler le fait que pour un test à 4 voies, nous nous attendons à ce que les deux produisent pretty des résultats similaires (~ 2,8 nanosecondes pour un if/else, ~ 2,0 pour switch).

3
Jerry Coffin

Notez que lorsqu'un commutateur n'est PAS compilé dans une table de saut, vous pouvez très souvent écrire si est plus efficace que le commutateur ...

(1) si les cas ont un ordre, plutôt que le pire test pour tous les N, vous pouvez écrire votre si pour tester si dans la moitié supérieure ou inférieure, puis dans chaque moitié de cela, style de recherche binaire ... résultant en le pire des cas étant logN plutôt que N(2) si certains cas/groupes sont beaucoup plus fréquents que d’autres cas, la conception de vos ifs pour les isoler d’abord peut accélérer le temps moyen écoulé

2
Brian Kennedy

Pas sûr pourquoi on est plus rapide et on est plus lent, cependant.

En réalité, ce n’est pas trop difficile à expliquer ... Si vous vous souvenez que les branches mal prédites sont des dizaines à des centaines de fois plus chères que les branches correctement prédites.

Dans le % 20 version, le premier cas/if est toujours celui qui frappe. Les processeurs modernes "apprennent" quelles branches sont généralement prises et lesquelles ne le sont pas. Ils peuvent ainsi facilement prédire comment cette branche se comportera à presque chaque itération de la boucle. Cela explique pourquoi la version "if" vole; il n'a jamais à exécuter quoi que ce soit après le premier test et il prédit (correctement) le résultat de ce test pour la plupart des itérations. Évidemment, le "commutateur" est implémenté légèrement différemment - peut-être même une table de saut, qui peut être lente grâce à la branche calculée.

Dans le % 21 _ version, les branches sont essentiellement aléatoires. Ainsi, non seulement beaucoup d’entre eux exécutent chaque itération, le processeur ne peut pas deviner le chemin qu’ils prendront. C'est le cas où une table de saut (ou une autre optimisation de "commutation") est susceptible de vous aider.

Il est très difficile de prédire comment un morceau de code va fonctionner avec un compilateur moderne et un processeur, et cela devient de plus en plus difficile à chaque génération. Le meilleur conseil est "ne même pas essayer; toujours profiler". Ce conseil s'améliore - et le nombre de personnes qui peuvent l'ignorer avec succès devient plus petit - chaque année.

Tout cela pour dire que mon explication ci-dessus est en grande partie une conjecture. :-)

2
Nemo

Non, c’est le cas si saute d’autre si saute d’autre ... Une table de saut aurait une table d’adresses ou utiliserait un hachage ou quelque chose comme ça.

Plus rapide ou plus lent est subjectif. Vous pouvez par exemple avoir le cas 1 comme dernière chose au lieu de la première et si votre programme de test ou programme dans le monde réel utilisait le cas 1 la plupart du temps, le code serait plus lent avec cette implémentation. Donc, réorganiser la liste des cas, en fonction de la mise en œuvre, peut faire une grande différence.

Si vous aviez utilisé les cas 0-3 au lieu de 1-4, le compilateur aurait peut-être utilisé une table de saut, le compilateur aurait donc dû supprimer votre +1 de toute façon. Peut-être était-ce le petit nombre d'articles. Si vous l'aviez définie entre 0 et 15 ou entre 0 et 31, par exemple, il aurait peut-être été implémenté avec une table ou utilisé un autre raccourci. Le compilateur est libre de choisir son mode d’implémentation dans la mesure où il remplit les fonctionnalités du code source. Et ceci entre dans les différences de compilateur et dans les différences de version et d'optimisation. Si vous voulez une table de saut, créez une table de saut, si vous voulez un arbre if-then-else, créez un arbre if-then-else. Si vous voulez que le compilateur décide, utilisez une instruction switch/case.

2
old_timer

Aucun. Dans la plupart des cas particuliers, lorsque vous entrez dans l'assembleur et effectuez de véritables mesures de performances, votre question est tout simplement fausse. Pour l’exemple donné, votre pensée est définitivement trop courte depuis

counter += (4 - counter % 4);

me semble être la bonne expression d’incrémentation que vous devriez utiliser.

1
Jens Gustedt