What a switch actually compiles to
You've heard that a switch can be faster than an if/else chain, and that it likes dense integer cases starting near zero. Both are true, and the Z80 shows you why in one move: a switch doesn't test your value at all — it uses it as an index to look up where to jump.
The slow way: ask, ask, ask
An if/else chain compares the value against each case in turn. Case 0 is quick; case 9 means nine comparisons and nine branches first. The cost grows with how far down the list you land — linear, and unpredictable:
cp 0
jp z, state_idle
cp 1
jp z, state_walk
cp 2
jp z, state_jump ; ... and on, and onThe fast way: compute the destination
Instead, lay the destination addresses out in a table. Take the value, double it (each address is two bytes), read the address from the table, and jump straight there. No comparisons — the value is the index:
ld l, a
ld h, 0
add hl, hl ; index × 2 (addresses are 2 bytes)
ld de, table
add hl, de ; hl -> the slot for this case
ld e, (hl)
inc hl
ld d, (hl)
ex de, hl
jp (hl) ; jump to the address we looked up
table: dw state_idle, state_walk, state_jump, ...Now the compiler’s habits make sense. It wants dense cases — 0,1,2,3 — because the value is a direct index into the table; gaps mean wasted slots or a fallback. Sparse cases like 1, 1000, 50000 can’t be a clean table, so it quietly falls back to comparisons or a binary search. That’s why “use an enum starting at zero” is real performance advice, not style.
A state machine, a bytecode interpreter’s dispatch loop, a
switchover an opcode — all the same trick: don’t ask a long list of questions when the answer can be an address you look up. The Z80 has noswitchkeyword, so you build the mechanism yourself and see precisely what your compiler has been doing all along.