Why Computed Goto Is Faster Than Switch in CPython
CPython executes Python code by compiling it into bytecode
instructions that are processed sequentially by a virtual machine
evaluation loop. Historically, this loop relied on a standard C
switch statement to direct execution to the appropriate
instruction handler. By switching to computed goto
dispatching—often referred to as direct call threading—CPython achieves
a noticeable speedup across bytecode execution. This performance
improvement is primarily driven by optimizing how modern CPU branch
predictors handle indirect jumps, drastically cutting down CPU pipeline
stalls caused by branch mispredictions.
The Switch Statement Bottleneck
In a naive bytecode interpreter, the execution loop is structured
around a while loop containing a large switch
statement:
while (1) {
switch (*pc++) {
case OP_LOAD_FAST:
// handle LOAD_FAST
break;
case OP_BINARY_ADD:
// handle BINARY_ADD
break;
// hundreds of other cases...
}
}At the machine-code level, the C compiler converts this
switch block into a jump table controlled by a single
indirect jump instruction located at the top of the loop. Every bytecode
handler finishes by jumping back to this central location, which then
reads the next opcode and branches to the corresponding handler.
Because every single opcode routes through the exact same indirect jump instruction, the CPU's hardware Branch Target Buffer (BTB) struggles to predict the target. In real-world software, opcodes vary wildly depending on the code path. When hundreds of different bytecode targets share a single branch instruction, the branch predictor frequently guesses wrong. A branch misprediction forces the CPU to flush its instruction pipeline, costing anywhere from 10 to 20 clock cycles per mispredicted opcode.
How Computed Goto Dispatching Works
Computed goto is a C language extension supported by
compilers like GCC and Clang. It allows taking the memory address of a
code label using the unary && operator and storing
that address in an array of pointers:
static void* dispatch_table[] = {
&&TARGET_LOAD_FAST,
&&TARGET_BINARY_ADD,
// ...
};Instead of returning to a central hub via break, every
individual opcode handler ends by fetching the next instruction and
jumping directly to it:
TARGET_LOAD_FAST:
// handle LOAD_FAST
goto *dispatch_table[*pc++];
TARGET_BINARY_ADD:
// handle BINARY_ADD
goto *dispatch_table[*pc++];This pattern is known as direct threaded code. Control never jumps back to a shared dispatch hub; execution moves directly from one bytecode handler to the next.
Why Direct Threading Outperforms Switch Loops
The performance advantage of computed goto dispatching
boils down to hardware branch prediction and reduced instruction
overhead:
- Distributed Branch Predictions: Instead of one central jump instruction handling every opcode transition, computed gotos distribute the dispatch logic across hundreds of independent indirect jumps (one at the end of each handler).
- Context-Aware Branch History: Modern CPU branch
predictors track branch history separately for each jump site. Certain
bytecode patterns occur in predictable sequences (for example,
LOAD_FASTis frequently followed by anotherLOAD_FASTor a binary operation). With distinct branch instructions at the end of each handler, the BTB can record and predict these sequence correlations accurately. - Elimination of Loop Overhead: A
switchimplementation incurs two jumps per opcode: an unconditional jump back to the top of the loop, followed by the indirect jump via the switch table. Computed gotos eliminate the first jump entirely, cutting the total instruction count required to advance the program counter.
Implementation in CPython
In CPython's evaluation loop (traditionally located in
Python/ceval.c, and generated via templates in newer
versions), this optimization is controlled by the
USE_COMPUTED_GOTOS macro. If the host compiler supports
label addressing, CPython configures the DISPATCH() macro
to use goto *opcode_targets[opcode]. If the compiler lacks
support—such as MSVC without specific extensions—CPython cleanly falls
back to the standard switch construct. When enabled,
computed gotos generally deliver a 15% to 25% reduction in dispatch
overhead, translating to a measurable 5% to 10% overall execution speed
improvement in pure bytecode interpretation.