if elseif vs. switch

I got the impression that if else if chains seem to often produce smaller code then switch statements. Has anyone an explanation why this is the case? I as somewhat curious. I would have expected that switch statements produce shorter code. Why is it the other way round?

Udo

One way to optimize switch statements is to use a jump table. The typical sequence is to perform a bit of math on the switch expression and then perform an indirect jump using the table.

I have no idea if the compiler performs this optimization but, if it does, that might explain the difference in code size.

Interesting observation, and something to keep in mind if you are needing to save a few precious bytes on a large program. I don't know why it would be, but I think as a developer I would rather see people use switch-case, than if-then-else, unless the situation demanded it because of being tight on space. The latter being less readable than the former, and a potential maintenance issue down the road (unless, as noted, its use can be justified - in which case the reasoning should be put in the comments, and plenty of comments in the code to help someone who isn't the original developer follow along).

:slight_smile:

@cr0sh: this is exactly why I converted most of my stuff to switch instead of if then else. However I wonder why this made my programs longer. Not that I am running out of space. I use a 644 and have still ~8k left. But I am still wondering.

Udo

It may be, as Coding Badly suggests, that switch-case gets compiled as a lookup table with jump offsets. That could explain the larger code, but I would much rather see this fixed in the code generator, than having to rewrite my code.

The comment below is from the Arduino bootloader source.

      /* A bunch of if...else if... gives smaller code than switch...case ! */

Then what is the nature of the larger code? Is it a fixed overhead for every switch-case statement or is a shared library segment used to handle the lookups? Is the difference significant for a small, large (how large) number of case entries?

The compiler is extremely good in optimizing things, understanding assingment of integers and their consequences, removing unused code. This works most likely not so well with the more systematic "cases".. Even without optimization, switches will have a small overhead, that will level out for 4 or more cases only. More cases should give shorter code..

I created a small benchmark comparing switch-case to if-else-if that revealed the following (using non sequential, single byte conditions):

1. Using 4 branches and a single set of if/switch constructions:
Size: If is smaller by 24 bytes
Time: If is faster by 25%

2. Using 8 branches and a single set of if/switch constructions:
Size: If is smaller by 16 bytes
Time: Switch is faster by 24%

3. Using 4 branches and a double set of if/switch constructions:
Size: If is smaller by 42 bytes
Time: If is faster by 27%

4. Using 8 branches and a double set of if/switch constructions:
Size: If is smaller by 30 bytes
Time: Switch is faster by 27%

5. Using 5 branches and a single set of if/switch constructions:
Size: If is smaller by 10 bytes
Time: Switch is faster by 17%

Generally I would prefer switch-case for anything with 4 or more branches for readability. Apparently this is neither fastest nor smallest on the Arduino.

From 5 branches onwards, switch is faster, however at a small code size penalty.

1 Like

a double set of if/switch constructions

Would you mind showing what that looks like?

Did your speed-testing code assume that all branches are equally likely?

The double sets of if/switch statements was to see if there was some shared lookup code used across switch statements. It appears not to be the case.

Benchmark is as follows:

/* switch-case vs if-else-if
 *
 */

//#define USE_TWO
//#define LARGE
#define USE_IF
//#define USE_SWITCH

void setup()
{
  Serial.begin(57600);
  Serial.println("Switch/If Benchmark Started");
  
  byte  tmp = 0;
  uint32_t t = millis();
  for (int i=0;i<10000;i++) {
    tmp += test((byte)i);
#ifdef USE_TWO    
    tmp += test2((byte)i);
#endif    
  }
  t = millis() - t;
#ifdef USE_IF  
  Serial.println("Using if-else-if");
#endif
#ifdef USE_SWITCH
  Serial.println("Using switch-case");
#endif
#ifdef LARGE
  Serial.println("Large");
#else 
  Serial.println("Small");
#endif
  Serial.print("Time: ");
  Serial.println(t, DEC);
  Serial.print("Value: ");
  Serial.println(tmp, HEX);
}

#ifdef USE_SWITCH
byte test(byte val)
{
  byte tmp = 0;

  switch (val) {
    case 'a':
      tmp = val + 1;
      break;
    case 'B':
      tmp = val + 2;
      break;
    case 'c':
      tmp = val + 3;
      break;
    case 'D':
      tmp = val + 4;
      break;
    case '8':
      tmp = val + 5;
      break;
#ifdef LARGE      
    case '6':
      tmp = val + 6;
      break;
    case '4':
      tmp = val + 7;
      break;
    case '2':    
      tmp = val + 8;
      break;
#endif      
  }
  return tmp;
}
#endif

#ifdef USE_IF
byte test(byte val)
{
  byte tmp = 0;
  
  if (val == 'a') {
    tmp = val + 1;
  } else if (val == 'B') {
    tmp = val + 2;
  } else if (val == 'c') {
    tmp = val + 3;
  } else if (val == 'D') {
    tmp = val + 4;
  } else if (val == '8') {
    tmp = val + 5;
#ifdef LARGE    
  } else if (val == '6') {
    tmp = val + 6;
  } else if (val == '4') {
    tmp = val + 7;
  } else if (val == '2') {
    tmp = val + 8;
#endif
  }
  return tmp;
}
#endif

#ifdef USE_TWO
#ifdef USE_SWITCH
byte test2(byte val)
{
  byte tmp = 0;

  switch (val) {
    case 'a':
      tmp = val + 1;
      break;
    case 'B':
      tmp = val + 2;
      break;
    case 'c':
      tmp = val + 3;
      break;
    case 'D':
      tmp = val + 4;
      break;
#ifdef LARGE      
    case '8':
      tmp = val + 5;
      break;
    case '6':
      tmp = val + 6;
      break;
    case '4':
      tmp = val + 7;
      break;
    case '2':    
      tmp = val + 8;
      break;
#endif      
  }
  return tmp;
}
#endif

#ifdef USE_IF
byte test2(byte val)
{
  byte tmp = 0;
  
  if (val == 'a') {
    tmp = val + 1;
  } else if (val == 'B') {
    tmp = val + 2;
  } else if (val == 'c') {
    tmp = val + 3;
  } else if (val == 'D') {
    tmp = val + 4;
#ifdef LARGE    
  } else if (val == '8') {
    tmp = val + 5;
  } else if (val == '6') {
    tmp = val + 6;
  } else if (val == '4') {
    tmp = val + 7;
  } else if (val == '2') {
    tmp = val + 8;
#endif
  }
  return tmp;
}
#endif
#endif

void loop()
{
  
}

Benchmark is as follows:

so most of the tests are on values that does not match any branch of the ifs or switches. In a more realistic scenario, I'd say few values would be unmatched, and the if's would be coded with the most likely value first.

(Can't test myself as I have no Arduino :cry: )

In a more realistic scenario ...

You are more than welcome to contribute with additonal benchmarks.

As becnhmarks go - you may be able to proove just about anything. :slight_smile:

You are more than welcome to contribute with additional benchmarks.

Alright, does the relative timings of if and switch change if you replace the for-loop in setup with this?

byte tvals[] = {'a','a','a','a','B','B','c','D'};
for (int i=0;i<10000;i++) {
    tmp += test(tvals[i % sizeof(tvals)]);
#ifdef USE_TWO    
    tmp += test2(tvals[i % sizeof(tvals)]);
#endif    
}

Running the modified version (using your set of 8 values) gave the following (using one set of 8 branches):

"If" drops from 25 to 17 ms and "Switch" goes up from 19 to 22 ms. So effectively this changes the benchmark in favor of "If" also in terms of speed.

What triggered my interest in this was the comment I had seen in the bootloader source and then the post on this topic. Apparently there is some benefit to using "if-else" (both speed and size), but then again there are a number of different optimizer levels and switches that are likely to be much more significant and possibly can they also skew this simple benchmark somewhat.