Avantage de basculer déclaration si-else

Quelle est la meilleure pratique pour utiliser une instruction switch plutôt que d’utiliser 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 de code, alors ne me détestez pas pour les conventions de nommage.

déclaration de 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; } 

if déclaration:

 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, vous ne perdrez donc rien. En cas de doute, placez d’abord les cas les plus courants dans l’instruction switch.

Dans le meilleur des cas, l’optimiseur peut trouver un meilleur moyen de générer le code. Les tâches courantes d’un compilateur sont de créer un arbre de décision binary (sauvegarde les comparaisons et les sauts dans le cas moyen) ou simplement créer une table de saut (fonctionne sans aucune comparaison).

Pour le cas particulier que vous avez fourni dans votre exemple, le code le plus clair est probablement:

 if (RequiresSpecialEvent(numError)) fire_special_event(); 

Évidemment, cela ne fait que déplacer le problème dans une autre zone 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 suggère pas que c’est la meilleure implémentation de CallsSpecialEvent, mais que c’est une option. Vous pouvez toujours utiliser un commutateur ou une chaîne if-else, ou une table de consultation, ou une manipulation de bits sur la valeur, peu importe. Plus votre processus de prise de décision est obscur, plus vous tirerez de la valeur de cette fonction isolée.

Le commutateur est plus rapide.

Essayez juste si / else-30 valeurs différentes dans une boucle, et comparez-le au même code en utilisant le commutateur pour voir combien le commutateur est plus rapide.

Maintenant, le commutateur a un problème réel : le commutateur doit connaître au moment de la compilation les valeurs dans 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 comstackra pas

La plupart des gens vont alors utiliser define (Aargh!), Et d’autres vont déclarer et définir 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 ; } } 

Au final, le développeur doit donc choisir entre “vitesse + clarté” et “couplage de code”.

(Pas qu’un changement ne puisse être écrit pour être aussi déroutant que l’enfer … La plupart des changements que je vois actuellement sont de cette catégorie “déroutante” “… Mais c’est une autre histoire …)

Modifier le 21/09/2008:

bk1e a ajouté le commentaire suivant: “La définition de constantes enums dans un fichier d’en-tête est un autre moyen de gérer cela”.

Bien sûr que oui.

Le but d’un type externe était de découpler la valeur de la source. La définition de cette valeur sous la forme d’une macro, d’une simple déclaration const int ou même d’une énumération a pour effet secondaire d’inclure la valeur. Par conséquent, si la valeur define, enum ou const int était modifiée, une recompilation serait nécessaire. La déclaration externe signifie qu’il n’est pas nécessaire de recomstackr en cas de changement de valeur, mais que, d’autre part, il est impossible d’utiliser le commutateur. La conclusion de l’ utilisation du commutateur augmentera le couplage entre le code du commutateur et les variables utilisées comme cas . Quand c’est Ok, alors utilisez le commutateur. Quand ce n’est pas le cas, alors pas de surprise.

.

Modifier 2013-01-15:

Vlad Lazarenko a commenté ma réponse, donnant un lien avec son étude approfondie du code d’assemblage généré par un commutateur. Très éclairant: http://741mhz.com/switch/

Comstackr va l’optimiser quand même – optez pour le commutateur car c’est le plus lisible.

Le commutateur, ne serait-ce que pour la lisibilité. Géant si les déclarations sont plus difficiles à maintenir et plus difficiles à lire à mon avis.

ERROR_01 : // chute intentionnelle

ou

(ERROR_01 == numError) ||

Le dernier est plus sujet aux erreurs et nécessite plus de saisie et de formatage que le premier.

Utilisez switch, c’est ce que c’est et ce que les programmeurs attendent.

Je mettrais les étiquettes de cas redondantes cependant – juste pour que les gens se sentent à l’aise, j’essayais de me rappeler quand / quelles règles sont pour les laisser tomber.
Vous ne voulez pas que le prochain programmeur travaille sur des détails de langue inutiles (cela pourrait être vous dans quelques mois!)

Code pour la lisibilité. Si vous voulez savoir ce qui donne de meilleurs résultats, utilisez un profileur, car les optimisations et les compilateurs varient, et les problèmes de performance sont rarement ceux où les gens le pensent.

IMO est un exemple parfait de ce qui a été fait pour changer

Les compilateurs sont vraiment bons pour optimiser le switch . GCC récent est également bon pour optimiser un tas de conditions dans un if .

J’ai fait quelques tests sur godbolt .

Lorsque les valeurs de case sont regroupées, gcc, clang et icc sont suffisamment intelligents pour utiliser une image bitmap pour vérifier si une valeur est l’une des valeurs spéciales.

Par exemple, gcc 5.2 -O3 comstack le switch vers (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 des données immédiates, il n’y a donc pas de risque que le cache de données y accède, ni de table de saut.

gcc 4.9.2 -O3 comstack le switch sur un bitmap, mais le 1U< with mov / shift. Il comstack la version if en série de twigs.

 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 mouvement). Cela lui permet de placer le bitmap dans un 32bit immédiat, en évitant le movabsq immédiat de 64 bits 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 de l'utilisation de jc fire_special_event est omniprésent et est un bogue du compilateur .)

rep ret est utilisé dans les twigs cibles et les twigs conditionnelles suivantes, au bénéfice des anciens AMD K8 et K10 (avant Bulldozer): que signifie «rep ret»? . Sans elle, la prédiction de twig ne fonctionne pas aussi bien sur ces processeurs obsolètes.

bt (bit test) avec un registre arg est rapide. Il combine le travail de décalage à gauche d'un bit 1 par errNumber et un test , mais il rest une latence d'un cycle et un seul uop Intel. Il est lent avec un argument de mémoire en raison de sa sémantique trop CISC: avec un opérande de mémoire pour la "chaîne de bits", l'adresse de l'octet à tester est calculée sur la base de l'autre arg (divisé par 8). 'limité au bloc de 1, 2, 4 ou 8 octets indiqué par l'opérande de mémoire.

À partir des tables d'instructions d'Agner Fog , une instruction de décalage à nombre 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).

Si vos affaires risquent de restr groupées dans le futur – si plusieurs affaires correspondent à un seul résultat – le changement peut s’avérer plus facile à lire et à maintenir.

Ils travaillent aussi bien. La performance est à peu près la même étant donné un compilateur moderne.

Je préfère les instructions if aux instructions de cas car elles sont plus lisibles et plus flexibles – vous pouvez append d’autres conditions non basées sur une égalité numérique, comme “|| max

changer est définitivement préférable. Il est plus facile de consulter la liste des cas d’un commutateur et de savoir avec certitude ce qu’il fait que de lire la condition longue.

La duplication dans la condition if est difficile pour les yeux. Supposons qu’un des == été écrit != ; remarqueriez-vous? Ou si une instance de ‘numError’ a été écrite ‘nmuError’, ce qui est juste arrivé à comstackr?

Je préfère généralement utiliser le polymorphism au lieu du commutateur, mais sans plus de détails sur le contexte, c’est difficile à dire.

En ce qui concerne la performance, le mieux est d’utiliser un profileur pour mesurer les performances de votre application dans des conditions similaires à celles attendues dans la nature. Sinon, vous optimisez probablement au mauvais endroit et dans le mauvais sens.

Je suis d’accord avec la compacité de la solution de commutation mais IMO vous détournez le commutateur ici.
Le but du commutateur est d’avoir un traitement différent en fonction de la valeur.
Si vous deviez expliquer votre algo en pseudo-code, vous utiliseriez un if car, sémantiquement, c’est ce que c’est: si any_error le fait
Donc, à moins que vous n’ayez l’intention de changer votre code pour avoir un code spécifique pour chaque erreur, j’utiliserais si .

Je ne suis pas sûr des meilleures pratiques, mais j’utiliserais switch – et ensuite piéger intentionnellement la chute via ‘default’

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 que la logique devienne un peu plus bête.

Je réalise que ça a l'air bizarre. Voici l'inspiration (d'après 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() 
 while (true) != while (loop) 

Le premier est probablement optimisé par le compilateur, ce qui expliquerait pourquoi la deuxième boucle est plus lente lorsque le nombre de boucles augmente.

Je choisirais la déclaration pour des raisons de clarté et de convention, même si je suis certain que certains seraient en désaccord. Après tout, vous voulez faire quelque chose if une condition est vraie! Avoir un switch avec une action semble un peu … inutile.

Veuillez utiliser le commutateur. L’instruction if prendra du temps proportionnellement au nombre de conditions.

Je ne suis pas la personne à vous parler de la vitesse et de l’utilisation de la mémoire, mais il est beaucoup plus facile de comprendre l’état de commutation qu’un énoncé important (en particulier deux ou trois mois plus tard).

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 un seul changement est nécessaire, implémentez explicitement la modification, il n’est pas nécessaire de modifier la valeur par défaut. Il est également beaucoup plus facile d’append ou de supprimer des cas à partir d’un SWITCH que de modifier IF et ELSEIF.

 switch(numerror){ ERROR_20 : { fire_special_event(); } break; default : { null; } break; } 

Peut-être même tester votre condition (dans ce cas, numerror) par rapport à une liste de possibilités, un tableau peut-être pour que votre SWITCH ne soit même pas utilisé à moins qu’il y ait un résultat définitif.

Voyant que vous n’avez que 30 codes d’erreur, codez votre propre table de sauts, alors vous faites vous-même 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 (à part la déclaration statique de la table de saut). De plus, avec un débogueur, vous pouvez modifier le comportement à l’exécution si vous en avez besoin, en plaçant directement les données de la table.

Je connais son ancien mais

 public class SwitchTest { static final int max = 100000; public static void main(Ssortingng[] 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); } } 

La variation du nombre de boucles change beaucoup:

Tandis que / else: 5ms Switch: 1ms Max Loops: 100000

Tandis que / else: 5ms Switch: 3ms Max Loops: 1000000

Tandis que / else: 5ms Switch: 14ms Max Loops: 10000000

Tandis que / else: 5ms Switch: 149ms Max Loops: 100000000

(append plus de déclarations si vous voulez)

En ce qui concerne la compilation du programme, je ne sais pas s’il y a une différence. Mais pour le programme lui-même et pour garder le code aussi simple que possible, je pense personnellement que cela dépend de ce que vous voulez faire. sinon, si les déclarations ont leurs avantages, je pense que:

vous permet de tester une variable sur des plages spécifiques que vous pouvez utiliser comme fonctions (Standard Library ou Personal).

(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, si les déclarations peuvent être compliquées et désordonnées (malgré vos meilleures tentatives). Les déclarations de changement 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: "< 

Je préfère les déclarations if - else if - else, mais c'est à vous de décider. Si vous voulez utiliser des fonctions comme conditions, ou si vous voulez tester quelque chose sur une plage, un tableau ou un vecteur et / ou si l'imbrication compliquée ne vous dérange pas, je vous recommande d'utiliser If else if else pour bloquer. Si vous voulez tester avec des valeurs uniques ou si vous voulez un bloc propre et facile à lire, je vous recommande d'utiliser les blocs de casse switch ().