switch Statements and Jump Tables
How a dense switch becomes a jump table while a sparse one becomes a chain of compares.
One Value, Many Destinations
A switch poses a question no single cmp can answer: "of these many cases, which one matches?" The compiler has two very different ways to implement it, and which one it picks depends entirely on the shape of the cases. Learning to recognise both, and to predict which the compiler will choose, is the last piece of reading control flow.
The two strategies are a chain of compares and a jump table. We will read each.
Dense Cases Become a Jump Table
When the case labels are packed close together, like 0, 1, 2, 3, 4, the compiler can do something clever. Instead of testing each value in turn, it uses the value as an index into a table of addresses and jumps directly to the right one. Here is a switch over five consecutive cases:
int dayKind(int day)
{
switch (day)
{
case 0: return 10;
case 1: return 20;
case 2: return 30;
case 3: return 40;
case 4: return 50;
default: return 0;
}
}
At -O0:
dayKind(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 4
ja .L2
mov eax, DWORD PTR -4[rbp]
lea rdx, 0[0+rax*4]
lea rax, .L4[rip]
mov eax, DWORD PTR [rdx+rax]
cdqe
lea rdx, .L4[rip]
add rax, rdx
jmp rax
.L4:
.L8:
mov eax, 10
jmp .L9
.L7:
mov eax, 20
jmp .L9
.L6:
mov eax, 30
jmp .L9
.L5:
mov eax, 40
jmp .L9
.L3:
mov eax, 50
jmp .L9
.L2:
mov eax, 0
.L9:
pop rbp
ret
This looks dense, but it is just three phases.
1. The bounds check. cmp DWORD PTR -4[rbp], 4 followed by ja .L2 checks whether day is within range. .L2 is the default case. Notice the jump is ja ("above"), from the unsigned family: by treating day as unsigned, a single comparison catches both day > 4 and day < 0 at once, because a negative int reinterpreted as unsigned becomes a huge number that is also "above" 4. This is a recurring compiler trick.
2. The table lookup. If day is in range, the next few lines compute an address. lea rax, .L4[rip] loads the address of the table .L4. The value day (in rax) is scaled and used to read an entry out of that table, which is then added back to the table's base. The exact arithmetic is fiddly, but the intent is simple: turn day into the address of the matching case.
3. The indirect jump. jmp rax is the payoff. This is an indirect jump: instead of a fixed label, it jumps to whatever address is held in rax. That address came from the table, so day == 2 lands directly on the case 2 code with no per-case comparisons at all.
The block at .L4: is the jump table itself, a list of code addresses. Each case body (.L8, .L7, ...) sets eax and jumps to the common exit .L9. The standout instruction to recognise is jmp rax: a jump whose target is a register, not a label, almost always means a jump table is in play.
Sparse Cases Become a Chain of Compares
A jump table only pays off when the cases are dense, because the table must have a slot for every value in the range. If the cases are far apart, say 1, 50, and 999, a table covering 1 through 999 would be almost entirely wasted entries. So the compiler falls back to the obvious approach: compare against each case one at a time.
int pick(int code)
{
switch (code)
{
case 1: return 100;
case 50: return 200;
case 999: return 300;
default: return 0;
}
}
pick(int):
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], edi
cmp DWORD PTR -4[rbp], 999
je .L2
cmp DWORD PTR -4[rbp], 999
jg .L3
cmp DWORD PTR -4[rbp], 1
je .L4
cmp DWORD PTR -4[rbp], 50
je .L5
jmp .L3
.L4:
mov eax, 100
jmp .L6
.L5:
mov eax, 200
jmp .L6
.L2:
mov eax, 300
jmp .L6
.L3:
mov eax, 0
.L6:
pop rbp
ret
There is no table and no jmp rax here. Instead it is a sequence of cmp/je pairs, one per case: compare code against 999, against 1, against 50, jumping to the matching body when equal, and falling through to .L3 (the default) when nothing matches. This is exactly what a hand-written chain of if/else if would produce, which is the point: a sparse switch is just an if/else if chain wearing different syntax.
Telling the Two Apart
When you open a switch in assembly, one glance settles which strategy the compiler used:
- See a
jmpto a register (jmp rax) and a block of addresses? That is a jump table: dense cases, constant-time dispatch regardless of how many there are. - See a run of
cmp/jepairs? That is a compare chain: sparse or few cases, checked one by one.
The trade-off is the same one a programmer would weigh by hand. A jump table reaches any case in the same few instructions but needs contiguous values; a compare chain handles any values but gets slower as cases pile up. The compiler measures the density of your cases and picks for you, and now you can read its decision straight off the listing.
Key Takeaways
- A
switchcompiles to either a jump table or a chain of compares, chosen by how dense the case values are. - A jump table dispatches in constant time via an indirect jump (
jmp rax) through a table of code addresses; the giveaway is a jump whose target is a register. - The bounds check before a jump table often uses an unsigned jump (
ja) to catch both too-high and negative values with one comparison. - Sparse or few cases compile to a compare chain of
cmp/jepairs, identical to anif/else ifladder. - Recognising
jmp raxplus an address block (table) versus repeatedcmp/je(chain) tells you instantly which strategy is in use.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
switch Statements and Jump Tables - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!