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:

  1. 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).
  2. 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_FAST is frequently followed by another LOAD_FAST or a binary operation). With distinct branch instructions at the end of each handler, the BTB can record and predict these sequence correlations accurately.
  3. Elimination of Loop Overhead: A switch implementation 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.