Une switch
déclaration est-elle réellement plus rapide qu'une if
déclaration?
J'ai exécuté le code ci-dessous sur le compilateur C ++ x64 de Visual Studio 2010 avec l' /Ox
indicateur:
#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 a obtenu ces résultats:
Instruction de commutation: 5261 ms Instruction If: 5196 ms
D'après ce que j'ai appris, les switch
instructions utilisent apparemment des tables de saut pour optimiser la ramification.
Des questions:
À quoi ressemblerait une table de saut de base, en x86 ou x64?
Ce code utilise-t-il une table de saut?
Pourquoi n'y a-t-il aucune différence de performances dans cet exemple? Existe-t-il une situation dans laquelle il existe une différence de performances 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
Mettre à jour:
Des résultats intéressants ici . Je ne sais pas pourquoi on est plus rapide et plus lent, cependant.
la source
5196 vs. 5261 shouldn't be enough to actually care
-> Je ne sais pas si vous avez mal compris la question ou si j'ai mal compris votre commentaire, mais ce n'est pas tout l'intérêt de ma question de demander pourquoi il n'y a pas de différence? (Ai-je jamais prétendu queRéponses:
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 d'une manière ou d'une autre.
C Le pseudocode pour une "table de sauts" serait quelque chose comme ceci - notez que le compilateur devrait en pratique insérer une forme de test if 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 série de nombres consécutifs.
Si le nombre de branches dans un commutateur est extrêmement élevé, un compilateur peut faire des choses comme utiliser la recherche binaire sur les valeurs du commutateur, ce qui (dans mon esprit) serait une optimisation beaucoup plus utile, car cela augmente considérablement les performances de certains scénarios, est aussi général 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 toute différence.
Pour répondre à vos questions spécifiques:
Clang en génère un qui ressemble à ceci :
Je peux dire qu'il n'utilise pas de table de saut - 4 instructions de comparaison sont clairement visibles:
Une solution basée sur une table de saut n'utilise pas du tout de comparaison.
EDIT 2014 : Il y a eu des discussions ailleurs de personnes familières avec l'optimiseur LLVM disant que l'optimisation de la table de saut peut être importante dans de nombreux scénarios; par exemple dans les cas où il y a une énumération avec de nombreuses valeurs et de nombreux cas contre des valeurs dans ladite énumération. Cela dit, je maintiens ce que j'ai dit ci-dessus en 2011 - trop souvent, je vois des gens penser "si je fais un changement, ce sera la même chose, peu importe le nombre de cas que j'ai" - et c'est complètement faux. Même avec une table de saut, vous obtenez le coût de 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 une échelle if / else if et le transformer en commutateur équivalent ou vice versa s'il serait plus rapide de le faire.
la source
switch
sorties. Soren a dit plusieurs autres choses que je voulais dire après avoir lu cette réponse.if
clauses a déjà été réglé à la main pour correspondre à la fréquence et aux besoins de performances relatives, où, comme celaswitch
est traditionnellement considéré comme une invitation ouverte à l'optimisation, selon le choix du compilateur. Bon point re sautant passéswitch
:-). La taille du code dépend des cas / plage - pourrait être meilleure. Enfin, certains énumérations, champs de bits etchar
scénarios sont intrinsèquement valides / limités et sans frais généraux.A votre question:
1. À quoi ressemblerait une table de saut de base, en x86 ou x64?
La table de saut est une adresse mémoire qui contient un pointeur sur les étiquettes dans quelque chose comme la structure d'un tableau. l'exemple suivant vous aidera à comprendre comment les tables de saut sont disposées
Où 00B14538 est le pointeur vers la table de sauts et une valeur comme D8 09 AB 00 représente le pointeur d'étiquette.
2.Ce code utilise-t-il une table de saut? Non dans ce cas.
3.Pourquoi n'y a-t-il aucune différence de performances dans cet exemple?
Il n'y a pas de différence de performances car les instructions pour les deux cas sont identiques, pas de table de saut.
4.Y a-t-il une situation dans laquelle il existe une différence de performance significative?
Si vous avez une très longue séquence de vérification if , dans ce cas, l'utilisation d'une table de saut améliore les performances (les instructions de branchement / jmp sont coûteuses si elles ne prédisent pas presque parfaitement) mais s'accompagnent du coût de la mémoire.
Le code de toutes les instructions de comparaison a également une certaine taille, donc en particulier avec les pointeurs ou les décalages 32 bits, une recherche de table de saut unique peut ne pas coûter beaucoup plus de taille dans un exécutable.
Conclusion: le compilateur est assez intelligent pour gérer un tel cas et générer des instructions appropriées :)
la source
gcc -S
sortie: une séquence d' entrées.long L1
/.long L2
table est plus significative qu'un hexdump, et plus utile à quelqu'un qui veut apprendre à regarder le compilateur. (Bien que je suppose que vous regardiez simplement le code du commutateur pour voir s'il s'agissait d'un jmp indirect ou d'un tas de jcc).Le compilateur est libre de compiler l'instruction switch en tant que code équivalent à l'instruction if, ou de créer une table de raccourcis. Il choisira probablement l'un sur l'autre en fonction de ce qui s'exécutera le plus rapidement ou générera un peu le plus petit code en fonction de ce que vous avez spécifié dans les options de votre compilateur - dans le pire des cas, ce sera la même vitesse que les instructions if
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 important, une table de sauts sera beaucoup plus rapide qu'une série de if. Cependant, si les étapes entre les valeurs sont très grandes, la table de sauts peut devenir grande et le compilateur peut choisir de ne pas en générer une.
la source
Comment savez-vous que votre ordinateur n'effectuait pas de tâche sans rapport avec le test pendant la boucle de test du commutateur et effectuait moins de tâches pendant la boucle de test if? Vos résultats de test ne montrent rien comme:
Mes résultats:
J'ai ajouté:
à la fin afin qu'il n'optimise pas la boucle car le compteur n'a jamais été utilisé dans votre exemple, alors pourquoi le compilateur effectuerait-il la boucle? Immédiatement, le commutateur a toujours été gagnant même avec un tel micro-benchmark.
L'autre problème avec votre code est:
dans votre boucle de commutation, contre
dans votre boucle if. Très grande différence si vous corrigez cela. Je crois que mettre la déclaration à l'intérieur de la déclaration switch provoque le compilateur à envoyer la valeur directement dans les registres CPU plutôt que de la mettre d'abord sur la pile. Ceci est donc en faveur de la déclaration switch 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 là-bas. Par nombre aléatoire, je veux dire un nombre basé sur l'heure actuelle, par exemple. Sinon, le compilateur pourrait transformer vos deux fonctions en une seule opération mathématique longue sans même vous soucier de boucles.
J'ai modifié le code de Ryan juste assez pour m'assurer que le compilateur ne pouvait pas comprendre les choses avant l'exécution du code:
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.
la source
print
déclaration? Je l'ai ajouté à la fin de l'ensemble du programme et je n'ai vu aucune différence. Je ne comprends pas non plus quel est le "problème" avec l'autre ... l'esprit d'expliquer quelle est la "très grande différence"?Un bon compilateur d'optimisation tel que MSVC peut générer:
En bref, si le commutateur semble plus lent qu'une série d'if, le compilateur peut simplement le convertir en un. Et il est probable que ce ne soit pas seulement une séquence de comparaisons pour chaque cas, mais un arbre de recherche binaire. Voir ici pour un exemple.
la source
Je vais répondre 2) et faire quelques commentaires généraux. 2) Non, il n'y a pas de table de saut dans le code d'assemblage que vous avez publié. Une table de saut est une table de destinations de saut et une ou deux instructions pour passer directement à un emplacement indexé à partir de la table. Une table de saut aurait plus de sens lorsqu'il existe de nombreuses destinations de commutation possibles. Peut-être que l'optimiseur sait que la logique simple sinon, est plus rapide, sauf si le nombre de destinations est supérieur à un certain seuil. Essayez à nouveau votre exemple avec disons 20 possibilités au lieu de 4.
la source
J'étais intrigué et j'ai regardé ce que je pouvais changer dans votre exemple pour lui permettre d'exécuter l'instruction switch plus rapidement.
Si vous obtenez 40 instructions if et ajoutez un cas 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 la suppression du cas 0 peut être vu ici: https://www.ideone.com/LFnrX .
la source
Voici quelques résultats de l'ancien benchmark bench ++ benchmark:
Ce que nous pouvons en voir, c'est que (sur cette machine, avec ce compilateur - VC ++ 9.0 x64), chaque
if
test prend environ 0,7 nanosecondes. À mesure que le nombre de tests augmente, les échelles de temps sont presque parfaitement linéaires.Avec l'instruction switch, il n'y a presque pas de différence de vitesse entre un test à 2 voies et un test à 10 voies, 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, c'est toujours mieux que le double de la vitesse d'un 10 voies
if
/else if
.Conclusion: utiliser uniquement un test à 4 voies ne vous montrera pas vraiment beaucoup sur les performances de
switch
vsif
/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 attendrions à ce que les deux produisent des résultats assez similaires (~ 2,8 nanosecondes pour unif
/else
, ~ 2,0 pourswitch
).la source
if
/ parelse
rapport à leur diffusion, etc. Impossible de trouver lesbench++
sources après 10 minutes googler.Notez que lorsqu'un commutateur n'est PAS compilé dans une table de saut, vous pouvez très souvent écrire if plus efficacement que le commutateur ...
(1) si les cas ont un ordre, plutôt que le pire des tests pour tous les N, vous pouvez écrire vos if 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, alors la conception de vos if pour isoler ces cas en premier peut accélérer le temps moyen à travers
la source
Non, ce sont si alors sauter autrement si alors sauter 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 pourriez par exemple avoir le cas 1 comme dernière chose au lieu de la première et si votre programme de test ou programme réel a utilisé le cas 1 la plupart du temps, le code serait plus lent avec cette implémentation. Donc, simplement réorganiser la liste des cas, selon 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 sauts, le compilateur aurait dû de toute façon supprimer votre +1. C'était peut-être le petit nombre d'articles. Si vous l'aviez fait 0 - 15 ou 0 - 31 par exemple, il peut l'avoir implémenté avec une table ou utilisé un autre raccourci. Le compilateur est libre de choisir comment il implémente les choses tant qu'il répond aux fonctionnalités du code source. Et cela entre dans les différences du compilateur et les différences de version et les différences d'optimisation. Si vous voulez une table de saut, faites une table de saut, si vous voulez un arbre si-alors-sinon faites un arbre si-alors-autre. Si vous voulez que le compilateur décide, utilisez une instruction switch / case.
la source
Ce n'est en fait 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 la
% 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, de sorte qu'elles peuvent facilement prédire comment cette branche se comportera à presque chaque itération de la boucle. Cela explique pourquoi la version "si" 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. De toute évidence, 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 la
% 21
version, les branches sont essentiellement aléatoires. Ainsi, non seulement beaucoup d'entre eux exécutent chaque itération, mais le processeur ne peut pas deviner dans quelle direction ils iront. C'est le cas où une table de saut (ou une autre optimisation de «commutateur») est susceptible de vous aider.Il est très difficile de prédire comment un morceau de code va fonctionner avec un compilateur et un processeur modernes, et cela devient plus difficile à chaque génération. Le meilleur conseil est "ne vous embêtez même pas à essayer; faites toujours le profil". Ce conseil s'améliore - et le nombre de personnes qui peuvent l'ignorer avec succès diminue - chaque année.
Tout cela pour dire que mon explication ci-dessus est en grande partie une supposition. :-)
la source
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 erronée. Pour l'exemple donné, votre réflexion est définitivement trop courte car
me semble être l'expression d'incrémentation correcte que vous devriez utiliser.
la source