Question

Quelle est la meilleure pratique pour utiliser une instruction switch ou utiliser une instruction si pour 30 énumérations unsigned où environ 10 ont une action attendue ( c'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éteste pas pour les conventions de nommage.

instruction :

// 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;       
}

si instruction:

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();
}
Était-ce utile?

La solution

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, inscrivez 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).

Autres conseils

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 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 qu'il s'agit de la meilleure implémentation de REQUISSpecialEvent, mais simplement que c'est 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 utilisation dans une fonction isolée.

Le commutateur est plus rapide.

Essayez si / sinon-ing 30 valeurs différentes à l'intérieur d'une boucle et comparez-le au même code en utilisant switch pour voir à quel point le switch est plus rapide.

À présent, le commutateur a un problème réel : le commutateur doit connaître les valeurs contenues dans chaque cas lors de la compilation. 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 compilera pas.

La plupart des gens utiliseront ensuite define (Aargh!), 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 ;
    }
}

Le développeur doit donc choisir entre "vitesse + clarté". vs "couplage de code".

(Ce n'est pas qu'un commutateur ne puisse pas être écrit pour semer la confusion ... La plupart des commutateurs que je vois actuellement appartiennent à cette "catégorie déroutante" ... mais ceci est une autre histoire ...)

  

Éditer 2008-09-21:

     

bk1e a ajouté le commentaire suivant: " La définition de constantes comme énumérations dans un fichier d'en-tête est un autre moyen de: gérer cette ".

     

Bien sûr que si.

     

Le type 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 L’utilisation de 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 le cas, pas de surprise.

.

  

Éditer le 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 interrupteur. Très éclairant: http://741mhz.com/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 : // échec intentionnel

ou

(ERROR_01 == numError) ||

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

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 tomber.
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 vous arriver dans quelques mois!)

Compilers are really good at optimizing switch. Recent gcc is also good at optimizing a bunch of conditions in an if.

I made some test cases on godbolt.

When the case values are grouped close together, gcc, clang, and icc are all smart enough to use a bitmap to check if a value is one of the special ones.

e.g. gcc 5.2 -O3 compiles the switch to (and the if something very similar):

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()

Notice that the bitmap is immediate data, so there's no potential data-cache miss accessing it, or a jump table.

gcc 4.9.2 -O3 compiles the switch to a bitmap, but does the 1U<<errNumber with mov/shift. It compiles the if version to series of 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()

Note how it subtracts 1 from errNumber (with lea to combine that operation with a move). That lets it fit the bitmap into a 32bit immediate, avoiding the 64bit-immediate movabsq which takes more instruction bytes.

A shorter (in machine code) sequence would be:

    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

(The failure to use jc fire_special_event is omnipresent, and is a compiler bug.)

rep ret is used in branch targets, and following conditional branches, for the benefit of old AMD K8 and K10 (pre-Bulldozer): What does `rep ret` mean?. Without it, branch prediction doesn't work as well on those obsolete CPUs.

bt (bit test) with a register arg is fast. It combines the work of left-shifting a 1 by errNumber bits and doing a test, but is still 1 cycle latency and only a single Intel uop. It's slow with a memory arg because of its way-too-CISC semantics: with a memory operand for the "bit string", the address of the byte to be tested is computed based on the other arg (divided by 8), and isn't limited to the 1, 2, 4, or 8byte chunk pointed to by the memory operand.

From Agner Fog's instruction tables, a variable-count shift instruction is slower than a bt on recent Intel (2 uops instead of 1, and shift doesn't do everything else that's needed).

IMO this is a perfect example of what switch fall-through was made for.

If your cases are likely to remain grouped in the future--if more than one case corresponds to one result--the switch may prove to be easier to read and maintain.

They work equally well. Performance is about the same given a modern compiler.

I prefer if statements over case statements because they are more readable, and more flexible -- you can add other conditions not based on numeric equality, like " || max < min ". But for the simple case you posted here, it doesn't really matter, just do what's most readable to you.

switch is definitely preferred. It's easier to look at a switch's list of cases & know for sure what it is doing than to read the long if condition.

The duplication in the if condition is hard on the eyes. Suppose one of the == was written !=; would you notice? Or if one instance of 'numError' was written 'nmuError', which just happened to compile?

I'd generally prefer to use polymorphism instead of the switch, but without more details of the context, it's hard to say.

As for performance, your best bet is to use a profiler to measure the performance of your application in conditions that are similar to what you expect in the wild. Otherwise, you're probably optimizing in the wrong place and in the wrong way.

I agree with the compacity of the switch solution but IMO you're hijacking the switch here.
The purpose of the switch is to have different handling depending on the value.
If you had to explain your algo in pseudo-code, you'd use an if because, semantically, that's what it is: if whatever_error do this...
So unless you intend someday to change your code to have specific code for each error, I would use if.

I'm not sure about best-practise, but I'd use switch - and then trap intentional fall-through via 'default'

Aesthetically I tend to favor this approach.

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

Make the data a little smarter so we can make the logic a little dumber.

I realize it looks weird. Here's the inspiration (from how I'd do it in 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)

Probably the first one is optimised by the compiler, that would explain why the second loop is slower when increasing loop count.

I would pick the if statement for the sake of clarity and convention, although I'm sure that some would disagree. After all, you are wanting to do something if some condition is true! Having a switch with one action seems a little... unneccesary.

Im not the person to tell you about speed and memory usage, but looking at a switch statment is a hell of a lot easier to understand then a large if statement (especially 2-3 months down the line)

I would say use SWITCH. This way you only have to implement differing outcomes. Your ten identical cases can use the default. Should one change all you need to is explicitly implement the change, no need to edit the default. It's also far easier to add or remove cases from a SWITCH than to edit IF and ELSEIF.

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

Maybe even test your condition (in this case numerror) against a list of possibilities, an array perhaps so your SWITCH isn't even used unless there definately will be an outcome.

Seeing as you only have 30 error codes, code up your own jump table, then you make all optimisation choices yourself (jump will always be quickest), rather than hope the compiler will do the right thing. It also makes the code very small (apart from the static declaration of the jump table). It also has the side benefit that with a debugger you can modify the behaviour at runtime should you so need, just by poking the table data directly.

I know its old but

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

Varying the loop count changes a lot:

While if/else: 5ms Switch: 1ms Max Loops: 100000

While if/else: 5ms Switch: 3ms Max Loops: 1000000

While if/else: 5ms Switch: 14ms Max Loops: 10000000

While if/else: 5ms Switch: 149ms Max Loops: 100000000

(add more statements if you want)

When it comes to compiling the program, I don't know if there is any difference. But as for the program itself and keeping the code as simple as possible, I personally think it depends on what you want to do. if else if else statements have their advantages, which I think are:

allow you to test a variable against specific ranges you can use functions (Standard Library or Personal) as conditionals.

(example:

`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";

However, If else if else statements can get complicated and messy (despite your best attempts) in a hurry. Switch statements tend to be clearer, cleaner, and easier to read; but can only be used to test against specific values (example:

`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;

I prefer if - else if - else statements, but it really is up to you. If you want to use functions as the conditions, or you want to test something against a range, array, or vector and/or you don't mind dealing with the complicated nesting, I would recommend using If else if else blocks. If you want to test against single values or you want a clean and easy to read block, I would recommend you use switch() case blocks.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top