Quelle est la meilleure pratique pour utiliser une instruction switch
par rapport à une instruction if
pour 30 énumérations unsigned
où environ 10 ont une action attendue (qui est actuellement la même action). Les performances et l’espace doivent être pris en compte mais ne sont pas critiques. J'ai extrait l'extrait, alors ne me déteste pas pour les conventions de nommage.
Déclaration switch
:
// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing
switch (numError)
{
case ERROR_01 : // intentional fall-through
case ERROR_07 : // intentional fall-through
case ERROR_0A : // intentional fall-through
case ERROR_10 : // intentional fall-through
case ERROR_15 : // intentional fall-through
case ERROR_16 : // intentional fall-through
case ERROR_20 :
{
fire_special_event();
}
break;
default:
{
// error codes that require no additional action
}
break;
}
Déclaration if
:
if ((ERROR_01 == numError) ||
(ERROR_07 == numError) ||
(ERROR_0A == numError) ||
(ERROR_10 == numError) ||
(ERROR_15 == numError) ||
(ERROR_16 == numError) ||
(ERROR_20 == numError))
{
fire_special_event();
}
Utilisez le commutateur.
Dans le pire des cas, le compilateur générera le même code qu'une chaîne if-else, pour ne rien perdre. En cas de doute, mettez d'abord les cas les plus courants dans l'instruction switch.
Dans le meilleur des cas, l’optimiseur trouvera peut-être un meilleur moyen de générer le code. Un compilateur construit généralement un arbre de décision binaire (enregistre les comparaisons et saute dans le cas moyen) ou simplement une table de saut (fonctionne sans aucune comparaison).
Dans le cas particulier que vous avez fourni dans votre exemple, le code le plus clair est probablement le suivant:
if (RequiresSpecialEvent(numError))
fire_special_event();
Évidemment, cela ne fait que déplacer le problème vers un autre domaine du code, mais vous avez maintenant la possibilité de réutiliser ce test. Vous avez également plus d'options pour le résoudre. Vous pouvez utiliser std :: set, par exemple:
bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}
Je ne dis pas que c'est la meilleure implémentation de REQUISSpecialEvent, c'est juste une option. Vous pouvez toujours utiliser un commutateur ou une chaîne if-else, une table de consultation ou une manipulation de bits sur la valeur, peu importe. Plus votre processus de décision devient obscur, plus vous valoriserez son intégration dans une fonction isolée.
Le commutateur est plus rapide.
Essayez si 30 autres valeurs différentes dans une boucle et comparez-la au même code en utilisant switch pour voir à quel point le switch est plus rapide.
Maintenant, le commutateur a un problème réel : le commutateur doit connaître au moment de la compilation les valeurs à l'intérieur de chaque cas. Cela signifie que le code suivant:
// WON'T COMPILE
extern const int MY_VALUE ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
ne pas compiler.
La plupart des gens utiliseront ensuite define (Aargh!), Tandis que d'autres déclareront et définiront des variables constantes dans la même unité de compilation. Par exemple:
// WILL COMPILE
const int MY_VALUE = 25 ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
En fin de compte, le développeur doit choisir entre "vitesse + clarté" et "couplage de code".
(Non pas qu'un commutateur ne puisse pas être écrit pour être déroutant comme l'enfer ... La plupart des commutateurs que je vois actuellement appartiennent à cette catégorie "déroutant" "... mais ceci est une autre histoire ...)
Edit 2008-09-21:
bk1e a ajouté le commentaire suivant: "Définir les constantes comme des énumérations dans un fichier d’en-tête est un autre moyen de gérer cela ".
Bien sûr que ça l'est.
Le point d'un type externe était de découpler la valeur de la source. Définir cette valeur comme une macro, comme une simple déclaration const int ou même comme une énumération a pour effet secondaire d’inclure la valeur. Ainsi, si la définition, la valeur enum ou la valeur const int change, une recompilation est nécessaire. La déclaration extern signifie qu'il n'est pas nécessaire de recompiler en cas de changement de valeur, mais rend d'autre part impossible d'utiliser switch. La conclusion étant Utiliser switch augmentera le couplage entre le code du switch et les variables utilisées comme cas. Quand c'est bon, alors utilisez switch. Quand ce n'est pas, alors, pas de surprise.
.
Edit 2013-01-15:
Vlad Lazarenko a commenté ma réponse, donnant un lien vers son étude approfondie du code d'assemblage généré par un commutateur. Très éclairant: http://lazarenko.me/switch/
Le compilateur l’optimisera quand même - optez pour le commutateur le plus lisible.
Le commutateur, ne serait-ce que pour la lisibilité. Géant si les déclarations sont plus difficiles à maintenir et à lire à mon avis.
ERROR_01: // chute volontaire
ou
(ERROR_01 == numError)
Cette dernière est plus sujette aux erreurs et nécessite plus de dactylographie et de mise en forme que la première.
Code pour la lisibilité. Si vous voulez savoir ce qui fonctionne le mieux, utilisez un profileur, car les optimisations et les compilateurs varient, et les problèmes de performances ne concernent que rarement les utilisateurs.
Utilisez switch, c’est à quoi cela sert et à quoi les programmeurs s’attendent.
Je mettrais cependant les étiquettes de cas redondantes - juste pour que les gens se sentent à l'aise, j'essayais de me rappeler quand/quelles étaient les règles pour les laisser de côté.
Vous ne voulez pas que le prochain programmeur qui travaille dessus doive penser de manière inutile aux détails de la langue (cela pourrait être vous dans quelques mois!)
Les compilateurs sont vraiment doués pour optimiser switch
. Le gcc récent permet également d’optimiser de nombreuses conditions dans un if
.
J'ai fait quelques cas de test sur godbolt .
Lorsque les valeurs case
sont regroupées les unes à côté des autres, gcc, clang et icc sont suffisamment intelligents pour utiliser un bitmap afin de vérifier si une valeur est une des valeurs spéciales.
par exemple. gcc 5.2 -O3 compile le switch
en (et le if
quelque chose de très similaire):
errhandler_switch(errtype): # gcc 5.2 -O3
cmpl $32, %edi
ja .L5
movabsq $4301325442, %rax # highest set bit is bit 32 (the 33rd bit)
btq %rdi, %rax
jc .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
Notez que le bitmap est une donnée immédiate. Par conséquent, aucun cache de données potentiel ne doit y accéder, ni une table de saut.
gcc 4.9.2 -O3 compile le switch
en bitmap, mais le 1U<<errNumber
avec mov/shift. Il compile la version if
en séries de branches.
errhandler_switch(errtype): # gcc 4.9.2 -O3
leal -1(%rdi), %ecx
cmpl $31, %ecx # cmpl $32, %edi wouldn't have to wait an extra cycle for lea's output.
# However, register read ports are limited on pre-SnB Intel
ja .L5
movl $1, %eax
salq %cl, %rax # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
testl $2150662721, %eax
jne .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
Notez comment il soustrait 1 de errNumber
(avec lea
pour combiner cette opération avec un déplacement). Cela lui permet d’ajuster le bitmap dans une version immédiate 32 bits, en évitant le movabsq
immédiat 64 bits-immédiat qui prend plus d’octets d’instruction.
Une séquence plus courte (en code machine) serait:
cmpl $32, %edi
ja .L5
mov $2150662721, %eax
dec %edi # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
bt %edi, %eax
jc fire_special_event
.L5:
ret
(L'échec à utiliser jc fire_special_event
est omniprésent et est n bogue du compilateur .)
rep ret
est utilisé dans les cibles de branche et dans les branches conditionnelles suivantes, au profit des anciens AMD K8 et K10 (pré-bulldozer): Que signifie "rep ret"? . Sans cela, la prédiction de branche ne fonctionne pas aussi bien sur ces processeurs obsolètes.
bt
(bit test) avec un registre arg est rapide. Il combine le travail consistant à décaler à gauche un bit de 1 par errNumber
et à effectuer un test
, tout en conservant une latence d'un cycle et un seul processeur Intel. C'est lent avec un argument de mémoire à cause de sa sémantique trop compliquée: avec un opérande de mémoire pour la "chaîne de bits", l'adresse de l'octet à tester est calculée en fonction de l'autre argument (divisé par 8), et isn ne vous limitez pas au bloc de 1, 2, 4 ou 8 octets pointé par l’opérande mémoire.
À partir de tables d'instructions d'Agner Fog , une instruction de décalage à compte variable est plus lente qu'un bt
sur un processeur Intel récent (2 uops au lieu de 1, et shift ne fait pas tout ce qui est nécessaire) .
OMI, c’est un exemple parfait de la raison pour laquelle le commutateur à basculement a été conçu.
Je suis d'accord avec la compacité de la solution de commutateur mais IMO vous êtes détournant le commutateur ici.
Le but du commutateur est d’avoir une manipulation différente en fonction de la valeur.
Si vous deviez expliquer votre algo dans un pseudo-code, vous utiliseriez un if parce que, sémantiquement, c’est ce qu’il est: si quoi que ce soit qui le fasse ...
Donc, à moins que vous n'entendiez un jour changer votre code pour avoir un code spécifique pour chaque erreur, je voudrais utiliser if.
while (true) != while (loop)
Le premier est probablement optimisé par le compilateur, ce qui expliquerait pourquoi la seconde boucle est plus lente lors de l'augmentation du nombre de boucles.
Esthétiquement, j'ai tendance à privilégier cette approche.
unsigned int special_events[] = {
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20
};
int special_events_length = sizeof (special_events) / sizeof (unsigned int);
void process_event(unsigned int numError) {
for (int i = 0; i < special_events_length; i++) {
if (numError == special_events[i]) {
fire_special_event();
break;
}
}
}
Rendez les données un peu plus intelligentes pour rendre la logique un peu plus idiote.
Je réalise que ça a l'air bizarre. Voici l'inspiration (de comment je le ferais en Python):
special_events = [
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20,
]
def process_event(numError):
if numError in special_events:
fire_special_event()
Je ne suis pas sûr de la meilleure pratique, mais j'utiliserais switch - puis je capturerais une chute intentionnelle via 'default'
Si vos cas sont susceptibles de rester groupés à l'avenir (si plusieurs cas correspondent à un résultat), le changement peut s'avérer plus facile à lire et à gérer.
Ils fonctionnent aussi bien. Les performances sont à peu près les mêmes étant donné un compilateur moderne.
Je préfère les déclarations if aux déclarations case, car elles sont plus lisibles et plus flexibles. Vous pouvez ajouter d'autres conditions non basées sur l'égalité, telles que "|| max <min". Mais pour le cas simple que vous avez posté ici, peu importe, faites ce qui vous est le plus lisible.
le commutateur est définitivement préféré. Il est plus facile de consulter la liste des cas d'un commutateur et de savoir exactement ce qu'il fait que de lire la condition si si.
La duplication dans la condition if
est dure pour les yeux. Supposons que l'un des ==
était écrit !=
; remarqueriez-vous? Ou si une instance de 'numError' était écrite 'nmuError', que vient-il de compiler?
Je préférerais généralement utiliser le polymorphisme plutôt que le commutateur, mais sans plus de détails sur le contexte, c'est difficile à dire.
En ce qui concerne les performances, le mieux est d’utiliser un profileur pour mesurer les performances de votre application dans des conditions similaires à celles que vous attendez de la vie. Sinon, vous optimisez probablement au mauvais endroit et dans le mauvais sens.
Je dirais utiliser SWITCH. De cette façon, il vous suffit de mettre en œuvre des résultats différents. Vos dix cas identiques peuvent utiliser la valeur par défaut. Si vous modifiez tout ce dont vous avez besoin est d’appliquer explicitement la modification, vous n'avez pas besoin de modifier la valeur par défaut. Il est également beaucoup plus facile d'ajouter ou de supprimer des cas dans un SWITCH que de modifier IF et ELSEIF.
switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}
Peut-être même testez-vous votre condition (dans ce cas-ci numerror) contre une liste de possibilités, un tableau afin que votre SWITCH ne soit même pas utilisé sauf s’il en résultera un résultat.
Étant donné que vous ne disposez que de 30 codes d'erreur, codez votre propre table de saut, puis vous faites tous les choix d'optimisation (le saut sera toujours le plus rapide), plutôt que d'espérer que le compilateur agisse correctement. Cela rend également le code très petit (mis à part la déclaration statique de la table de saut). Cela présente également l’avantage supplémentaire qu’avec un débogueur, vous pouvez modifier le comportement au moment de l’exécution si vous en avez besoin, simplement en copiant directement les données de la table.
En ce qui concerne la compilation du programme, je ne sais pas s'il y a une différence. Mais en ce qui concerne le programme lui-même et le code aussi simple que possible, je pense personnellement que cela dépend de ce que vous voulez faire. if else, sinon les déclarations ont leurs avantages, ce qui, à mon avis, sont:
vous permettent de tester une variable par rapport à des plages spécifiques, vous pouvez utiliser des fonctions (Bibliothèque standard ou Personnel) comme conditionnelles.
(Exemple:
`int a;
cout<<"enter value:\n";
cin>>a;
if( a > 0 && a < 5)
{
cout<<"a is between 0, 5\n";
}else if(a > 5 && a < 10)
cout<<"a is between 5,10\n";
}else{
"a is not an integer, or is not in range 0,10\n";
Cependant, dans le cas contraire, les déclarations risquent de devenir compliquées et désordonnées (malgré vos meilleures tentatives) rapidement. Les déclarations de substitution ont tendance à être plus claires, plus propres et plus faciles à lire; mais ne peut être utilisé que pour tester des valeurs spécifiques (exemple:
`int a;
cout<<"enter value:\n";
cin>>a;
switch(a)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
cout<<"a is between 0,5 and equals: "<<a<<"\n";
break;
//other case statements
default:
cout<<"a is not between the range or is not a good value\n"
break;
Je préfère si - sinon si - autres déclarations, mais cela dépend vraiment de vous. Si vous souhaitez utiliser les fonctions en tant que conditions ou si vous souhaitez tester quelque chose par rapport à une plage, un tableau ou un vecteur et/ou si vous ne vous occupez pas de l'imbrication compliquée, je vous recommanderais d'utiliser If else si else bloque. Si vous voulez tester des valeurs uniques ou si vous voulez un bloc propre et facile à lire, je vous recommande d'utiliser des blocs de casse switch ().
Je connais son vieux mais
public class SwitchTest {
static final int max = 100000;
public static void main(String[] args) {
int counter1 = 0;
long start1 = 0l;
long total1 = 0l;
int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;
start1 = System.currentTimeMillis();
while (true) {
if (counter1 == max) {
break;
} else {
counter1++;
}
}
total1 = System.currentTimeMillis() - start1;
start2 = System.currentTimeMillis();
while (loop) {
switch (counter2) {
case max:
loop = false;
break;
default:
counter2++;
}
}
total2 = System.currentTimeMillis() - start2;
System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);
System.exit(0);
}
}
Varier le nombre de boucles change beaucoup:
While if/else: 5ms Commutateur: 1ms Maxi de boucles: 100000
While if/else: 5ms Commutateur: 3ms Boucles maxi: 1000000
While if/else: 5ms Commutateur: 14ms Boucles maxi: 10000000
Pendant si/autre: 5ms Commutateur: 149ms Boucles maximum: 100000000
(ajoutez plus de déclarations si vous voulez)
Je choisirais la déclaration if par souci de clarté et de convention, bien que je sois certain que certains ne seraient pas d’accord. Après tout, vous voulez faire quelque chose if
une condition est vraie! Avoir un commutateur avec une action semble un peu… inutile.
S'il vous plaît utilisez le commutateur. La déclaration if prendra du temps proportionnellement au nombre de conditions.
Je ne suis pas la personne pour vous parler de la vitesse et de l'utilisation de la mémoire, mais regarder un message sur un commutateur est beaucoup plus facile à comprendre qu'un énoncé if si important (surtout 2 ou 3 mois plus tard)