A switch looks innocent — a value, a list of cases, jump to the one that matches. But it
hands the compiler a genuine engineering decision, because there is no single right way to turn
"pick one of many" into machine code. Should it test the cases one by one? Binary-search them? Or leap
straight to the answer through a table of addresses? Each is correct; each is fastest for a different
shape of switch. Deciding which — from the number of cases and how tightly their
values cluster — is one of the most visibly clever things a code generator does.
Every strategy is built from primitives you already know: conditional jumps and labels from
Consider a plain source switch on an integer, with a handful of case labels and a
default. The compiler weighs three canonical lowerings:
| Strategy | Shape of generated code | Comparisons to dispatch | Chosen when… |
|---|---|---|---|
| Linear if–else chain | test each case value in turn | few cases (roughly | |
| Binary search tree | compare against a median, recurse into half | many cases, values sparse / scattered | |
| Jump table | index an array of addresses, one indirect jump | many cases packed into a dense range |
The figure shows all three lowerings of the same six-case switch, so you can see the comparison count shrink from a chain, to a tree, to a single indexed jump.
The simplest lowering is a straight cascade of conditional jumps — exactly what a chain of
if/else if would produce:
Each test that fails falls through to the next. For switch with a hot common case, putting
that case first is a real speed-up. Beyond three or four arms, though, the linear scan starts to sting,
and the compiler reaches for something smarter.
When there are many cases but their values are scattered — say
Each comparison halves the remaining candidates, so dispatch costs
switch over scattered enum-like
constants).
The prize lowering. If the case values fill a dense range — say
Slots for missing values in the range simply point at Ldefault. The catch is density:
a table for cases
Here is the heart of the compiler's choice: given the set of case values, measure the range density and pick a strategy. We run it on a dense switch, a sparse one, and a tiny one.
The dense case clears the threshold and gets an
Because goto *addr at the heart of a jump table is a indirect branch the CPU's
branch predictor struggles to guess. A mispredicted indirect jump can cost a dozen-plus cycles as the
pipeline flushes, whereas a short if–else chain is a string of direct conditional branches the
predictor handles beautifully, especially if one case dominates. So for a hot switch with a heavily
skewed distribution, a compiler (or a profile-guided optimiser) may deliberately keep a linear chain, or
peel the common case out in front of the table. Big-O is the start of the story, not the end;
the memory system and the branch predictor write the last chapter.
Two traps snare people lowering switches. First, in C-family languages a case without a break
falls through into the next case's body — that is control flow inside the arms,
totally separate from how the switch dispatches to an arm. Your lowering strategy
(chain / tree / table) picks the entry label; fall-through is just the absence of a jump at the end of an
arm. Conflating them produces code that runs the wrong arms. Second, the default is not
merely "the else at the bottom of a chain" — in a jump table it must fill every empty slot in the
range and catch every out-of-range value, which is why the lowering always emits the two range
checks before indexing. Forget either the empty-slot filler or a range check and an unlisted
value will index off the end of the table and jump to a garbage address.