Most of the people know that switch works like if-else blocks. The only difference between the switch and if-else is its being for the many conditional states, and also writing switch is more simple than if-else block. This knowledge is really a big   mistake  because switch has a different algorithm and working-procedure comparing to if-else block. To demonstrate this, I wrote a simple example which indicates the performance differences between switch and if-else in many different situations.


First let me explain the details of this Example. In the example, there are three arguments, which we are using in our tests.

These arguments are as follows;

# of iterations: Indicates that how many times we are testing the same situation. We cannot understand the difference of time by only one test because of the CPU’s speed. CPU will do the switch or if-else operations in a very short time. So to understand the real difference, we need to do the same test again and again. Then we have to check the time difference in total.
Parameter (Search Value): In the example, there is an if-else or a switch block which is searching from 1 – [# of cases]. Here the Parameter will indicates that in which case or else-if block our value is in. If you give the parameter a value of 1, this means that first case or first if block will return true. If you give a 99, this means that the 99th case or else-if block will return true. Actually this is the number of how many if-else conditions will be checked.
# of cases: This number indicates the total number of the conditions, which means how many cases or if-else block we have.

After giving these arguments; if you click to the Run Test button, it will make a test by using the given arguments and add a line to the results, which indicates properties and results of the test.

The above picture is showing the results of my 7 tests. Now let’s speak about these tests and than let me explain why the result is this way.
In the tests 1, 2 and 3 the arguments of the tests are same. The only difference is the Search Value which I try to select from the beginning, from the middle and finally from the end of the conditional block. As we see in the column for the switch, it does not matter what we are searching for. The time is nearly the same for each test (88, 86, 87). But for the if-else block, the time will dramatically increase according to the Search value. If we are searching for a number which is at the bottom of the if-else block, it takes too long; and if we select a number which is from the beginning of if-else block, it takes less time. But even for the smaller and for the bigger numbers, switch is always faster than if-else block.

To talk about other tests, we only change the # of case. This means the only difference is the number of the total conditional operations. But again the results show us that the switch is faster than if-else block, and number of conditions it is very important for the if-else block but not very important for the switch.

As I say at the beginning, most of the people think that switch works same as if-else block but this test shows us it is not true. So how does switch work?

Switch has a very simple but good algorithm. When we write a switch, it will automatically loads the first case’s value, then calculates the index number of the target case block, and adds an array of addresses of case block in the IL. For example:

public void GetSwitchResults(int value)
{
    switch (vaue)
    {
        case 1:
            return 1;
        case 2:
            return 2;
        case 3:
            return 3;
        case 4:
            return 4;
        case 5:
            return 5;
        default:
            return int.MinValue;
    }
}
.methodpublichidebysig newslot 
virtual final instance int32 
GetSwitchResult(int32 value) cil managed
{
     // Code Size: 63 byte(s)
     .maxstack 2
     .locals init (
          int32 num1,
          int32 num2)
     L_0000: nop 
     L_0001: ldarg.1 
     L_0002: stloc.1 
     L_0003: ldloc.1 
     L_0004: ldc.i4.1 // loads the first cases value (1)
     L_0005: sub // calculation [our value - first value]
     L_0006: switch (L_0021,L_0025,L_0029,L_002D,L_0031)
     L_001f: br.s L_0035 // address of default case
     L_0021: ldc.i4.1 // case 1
     L_0022: stloc.0 
     L_0023: br.s L_003D
     L_0025: ldc.i4.2  // case 2
     L_0026: stloc.0 
     L_0027: br.s L_003D
     L_0029: ldc.i4.3 // case 3
     L_002a: stloc.0 
     L_002b: br.s L_003D
     L_002d: ldc.i4.4 // case 4
     L_002e: stloc.0 
     L_002f: br.s L_003D
     L_0031: ldc.i4.5 // case 5
     L_0032: stloc.0 
     L_0033: br.s L_003D
     L_0035: ldc.i4 -2147483648 // default case
     L_003a: stloc.0 
     L_003b: br.s L_003D
     L_003d: ldloc.0 
     L_003e: ret 
}

IL codes generated by Xenocode Fox 2007 

The IL output of the left code above will look like as the right one. When compiling the code, switch puts a target for each case. And during runtime when we give a number to the switch, it checks the difference between the first case and our value, and the result indicates the index of the target case. So if the index is in one of the cases, it directly branches to the given case’s address. Actually the calculation formula is [our value] – [first case’s value], which returns us the index of our target case in the target cases array. You can see this in the IL. During the compile, compiler will generates the needed calculation codes just before the switch.

Let me explain it a little bit more with an example: Considering the code above, if we give a value 2, it will do the following action. 2 – 1 = 1. So second address (because of 0 based indexing) in the targets list is the starting address of our case block. So it directly goes to the L_0025 which the case 2:begins. Considering the value 3, the calculation will be like 3 – 1 = 2. So it goes to the third address in the target list, and so this address is L_0029(begins of the case 3).

There are 2 problems here. One of them is about the default case because although there are 5 cases and one default case (total 6), there are only 5 targets in the addresses array. And the second problem is what happens if the number does not fall between in any of the cases (for ex: 23, it does not exists in the cases list). For the first problem: If there is a default case in a switch, compiler will adds abr.s [TARGET] after the switch instruction which the TARGET indicates the starting address of the default case. If there is no default case, compiler will again add a br.s [TARGET] after the switch, but this time TARGET will indicates the ending address of switch.

For the second problem: After the calculation, if the result (index of the target case) is not a valid index for our target cases array, then switch does not do anything, so the next instruction (which br.s [TARGET] as we explain above) will execute and it will directly go to the default case if exists; otherwise it goes out of the switch. For example if the value is -1, then the calculation will be -1 – 1 = -2 which is not a valid index. So this value will directly go to the default case. It is also the same as for the value 6. After the calculation, the index will be 5 but our targets array has only 5 elements, so 5 is not a valid index for our switch again.

Now let’s go into the switch deeper. As we see in these examples, there might be a subtract operation on the switch condition to make the first value as zero. So this means that switch condition must be an integral type, which can be int16, int32, int64, enum etc. You cannot use float or double values in the switch cases. For example, the following code will not compile:

switch (a) // a is float
{
     case 0.1f:
         break;
     case 0.2f:
         break;
}

Compiler won’t compile the switches as how we write them. It may add some codes to the switch or may directly remove the switch and instead of it, it adds an if-else block to the IL. This will be done by the compiler automatically according to the number and values of the switch’s cases and condition of the switch. Check the following examples:

CodeAfter Compile
switch (a)
{
    case 1:
        break;
    case 3:
        break;
    case 4:
        break;
    case 5:
        break;
}
switch (a)
{
    case 1:
        break;
    case 2:
        break;
    case 3:
        break;
    case 4:
        break;
    case 5:
        break;
}

Switch cases must increase one by one continuously. If one item could not be found (here case 2), compiler will add an empty case for that number. Because of the calculation each element indicates a target. So there must be a target for the case 2, otherwise switch algorithm will not work. But this doesn’t mean that every time we use switch compiler fills the empty cases. In some situations, it can convert the switch to if – else block. Here it is not important for the compiler how we write the orders of the cases. It will first sorts the cases according to the case values, and then it will check the empty spaces.

CodeAfter Compile
switch (a)
{
    case 5:
        break;
    case 24:
        break;
    case 28:
        break;
}
if (a == 5)
{

}else if (a == 24)
{

}else if (a == 28)
{

}

Here in this example, compiler must add too many cases to fill all the empty spaces but instead of adding cases, compiler will compile this code as if-else block.

What about char and string cases? Can we use them or not?
For the char cases, I can say that yes. Because chars can be converted to int, so compiler can use char types in the switches without doing anything.

For the string cases, I can say again yes but this not common. That’s to say, again compiler decide this. In some cases, it converts them to if – else block (especially if the number of cases is small, for ex- 3 cases). And in some cases it will compile them as switch cases. So how can it be? As I said before, switches need integral types as cases conditions, and string is not an integral type. So how can this take place?

For the strings, compiler adds a Dictionary<string, int> item to a Compiler Generated class in that assembly. Then it will add the string values as string into that dictionary as int KeyValuePair which string is the case value and int is the order of the case. And during the runtime it will first try to get the int value of that string from the dictionary, and then use that value in the switch.

Anyway using switches (for more conditional operations) in the .NET will be better than using if – else block. Of course, if the conditions are constant values and type of the value is one of the integral types or string.

Share: