DEV Community

Cover image for What a Switch Tells the Compiler that If-Else Doesn't
Prachi Jha
Prachi Jha

Posted on

What a Switch Tells the Compiler that If-Else Doesn't

My first reaction to switch-case in 11th grade was a single word: why.

The if-else conditional had a perfectly easy, perfectly understandable syntax. And as far as my understanding went, they pretty much did the same thing.

It was not until I studied compilers and systems during my undergraduate that I realized switch statements are not merely an alternate syntax for if-else chains. They communicate additional information to the compiler, which can enable optimizations that may not be as obvious from an equivalent if-else sequence.
How so?

For the longest time, I believed that switch-case does exactly what an if-else does. We have a few conditions, we pick one of the cases, execute it, and come back. So it didn't really make sense to me about why we would need a break after every case.

Unless you write it for every case until you reach default, it would execute every code block for every single case written below the case executed too!

For example:

switch (nod){
    case 1:
        cout << "Yes" << endl;
    case 2:
        cout << "No" << endl;
    default:
        cout << "Invalid" << endl;
Enter fullscreen mode Exit fullscreen mode

A selection of nod = 1 would print YesNoInvalid instead of just yes. Which is why we use breaks to exit the conditional block.

switch (nod){
    case 1:
        cout << "Yes" << endl;
        break;
    case 2:
        cout << "No" << endl;
        break;
    default:
        cout << "Invalid" << endl;
Enter fullscreen mode Exit fullscreen mode

Neither is it efficient, nor is it a popular use case. The fallthrough makes more sense once you think of cases as jump destinations, not conditions. My mistake was assuming that switch-case is just if-else with a different syntax, and functionality probably remains the same.

However, a switch exposes a much lower-level control flow model than if-else.

If you squint carefully, the structure of a switch looks remarkably similar to low-level control flow. Rather than describing a sequence of conditions, it describes a dispatch from one value to one of several possible destinations. Thinking about a switch this way makes its fallthrough behavior much easier to understand.

For if-else, you describe the logic. The compiler constructs the control flow. For switch-case, you partially describe the control flow itself. The compiler gets much more structure to work with.

That said, it carries more information to the compiler, in a way, than the typical if-else.

In the above example, for if-else, the compiler knows:
Condition 1: nod == 1
Condition 2: nod == 2
Condition 3: otherwise

To realize this is a dispatch problem, the compiler has to analyze the conditions and notice:

  • Same variable
  • Compared to constants
  • Mutually exclusive

Now look at the switch example. We are basically telling the compiler that there is one controlling expression - 'nod'. The compiler can immediately construct:

Value Destination

1 case1
2 case2
3 case3

  • without doing any analysis. The source code is already expressing a dispatch relationship between values and destinations.

A switch ensures that there is a single expression compared against constant cases to select one destination. It is essentially carrying metadata about the coder's intent.

A switch statement describes a dispatch problem, which allows the compiler to implement it using various optimization techniques.

While experimenting, I repeatedly ran into a different problem: GCC kept deleting my switch statements entirely. In one example, the compiler noticed that every case returned the same value as the case label itself and rewrote the logic into a much simpler form.

In one experiment, I wrote:

case 1: return 1;
case 2000: return 2000;

expecting to study switch lowering. Instead, GCC noticed that the returned value was identical to the matched case value and generated a much simpler implementation. The switch effectively disappeared.

This was an important lesson: by the time we inspect assembly, we are often looking at the result of many optimization passes rather than a direct translation of the source code.

In fact, a switch statement does not necessarily survive long enough to be lowered at all. If the compiler can prove something stronger about the program, it may simplify or eliminate the switch entirely.

However, when a switch does survive to the lowering stage, the compiler still has several possible ways to implement it. This process, known as switch lowering, involves choosing a concrete control-flow strategy for the multiway branch.

A few common choices used by compilers are:

  1. Compare Chain Here
switch(x) {
case 1:
case 2:
case 3:
}
Enter fullscreen mode Exit fullscreen mode

becomes

cmp x, 1
je case1

cmp x, 2
je case2

cmp x, 3
je case3

jmp default
Enter fullscreen mode Exit fullscreen mode

Good when there are only a few cases, or when the case values are sparse.

  1. Jump Table

For denser values or larger numbers of cases, the compiler may generate a jump table. It however consumes memory.

index = x - 1

table:
0 -> case1
1 -> case2
2 -> case3
3 -> case4
4 -> case5
Enter fullscreen mode Exit fullscreen mode
  1. Decision Tree For many sparse values like 100, 200, 500, 800 - the compiler might emit a decision tree equivalent to a binary search tree. It is good for when there are many sparse values constructing a jump table for that would be huge.

A few other optimizations also include bit test, range checks and special transforms.

Now, how does the compiler know which optimization route to take? Based on heuristics.

Most compilers look at the number of cases, range and density.

For example:
Number of cases = 5
Range = 1..5
Density = 5/5 = 100%

If the density is high, jump table is probably good.

Another example:
case 1
case 1000
case 1000000

Here:
Cases = 3
Range = 1..1000000
Density ≈ 0

Jump tables would waste memory. Comparisons are probably better.

When the code compiles, the compiler often doesn't know several details like how often a branch will be executed, what values the user will enter, etc. But it still needs to decide optimizations, and whether to generate a jump table, a decision tree, etc. So it makes an educated guess.

Each compiler uses their own heuristics. For example, in GCC's source code, depending on the number of cases, their range, density, and even special patterns in the values, GCC may choose a comparison chain, a decision tree, a jump table, or an entirely different transformation. The compiler is not simply translating a switch statement into machine code; it is evaluating multiple possible implementations and selecting the one its heuristics believe will be most efficient.

Here is an example of different assembly code generated for the same decision problem using switch-case and if-else

int decision(int x){
        switch(x){
                case 1: return 10;
                case 100: return 20;
                case 200: return 30;
                case 300: return 40;
                case 400: return 50;
                case 500: return 60;
                case 600: return 70;
                case 700: return 80;
                case 800: return 90;
                case 900: return 100;
                default: return 0;
        }
}
Enter fullscreen mode Exit fullscreen mode

The simplified version of the assembly code generated for this is:

cmp x, 500
je  case500

jg  right_half
jl  left_half

left_half:
    cmp x, 200
    je case200

    cmp x, 300
    ...
Enter fullscreen mode Exit fullscreen mode

Notice how the compiler starts by comparing directly to 500 and then jumping in two different directions on the basis of the result. This is a decision tree.

GCC essentially built

                500
              /     \
          <500       >500
          /             \
       200             800
      /   \           /   \
    ...   ...      ...   ...
Enter fullscreen mode Exit fullscreen mode

Let us try the same with if-else using the following code:

int decision(int x){
        if (x == 1)
                return 10;
        else if (x == 100)
                return 20;
        else if (x == 200)
                return 30;
        else if (x == 300)
                return 40;
        else if (x == 400)
                return 50;
        else if (x == 500)
                return 60;
        else if (x == 600)
                return 70;
        else if (x == 700)
                 return 80;
        else if (x == 800)
                 return 90;
        else if (x == 900)
                return 100;
        else
                return 0;
}
Enter fullscreen mode Exit fullscreen mode

A simplified version of the assembly generated for this is:

cmp x, 1
je case1

cmp x, 100
je case100

cmp x, 200
je case200

cmp x, 300
je case300

...

cmp x, 900
je case900

jmp default
Enter fullscreen mode Exit fullscreen mode

In this particular experiment, GCC generated a decision tree for the switch version and a linear comparison chain for the if-else version.

Switch isn't merely alternate syntax for if-else. It communicates additional semantic information to the compiler: that a single value is being dispatched to one of several destinations. That additional structure allows the compiler to apply optimizations such as jump tables, decision trees, bit tests, and other lowering strategies.

What surprised me most while exploring GCC's implementation was that the question "Is switch faster than if-else?" is actually the wrong question. In many cases, the compiler may generate nearly identical code for both. In other cases, it may completely transform the control flow into a jump table, a decision tree, or something even more specialized. The answer depends on the distribution of case values, the number of branches, target architecture, and compiler heuristics.

The real advantage of a switch statement is not that it is inherently faster. It is that it expresses intent more clearly. By telling the compiler that we are performing a multiway dispatch on a single value, we give it the information needed to choose the most efficient implementation. As it turns out, a switch statement is not just a language feature - it is a hint to the optimizer.

Top comments (0)