Programming Languages & Computer Science

How Python 3.15 Profiles Itself Without Slowing Down

Python's new 3.15 release packs a JIT compiler, and the trick that makes it viable is a clever profiling mode so light-touch that it costs under five times the original speed — a fraction of the overhead older systems paid.

Every Python interpreter works the same basic way: it reads a bytecode instruction, looks up what to do, runs it, and fetches the next one. The bridge between "which instruction is next" and "run it" is a dispatch table — an array mapping each opcode number to the piece of code that handles it. CPython 3.15 turns this ordinary bookkeeping structure into the engine of its profiling.

The old ways of adding profiling were a compromise. You could duplicate the whole interpreter so one copy stays pure and the other logs everything, but that doubles the C binary and hurts CPU cache performance. Or you could sprinkle a single boolean flag like if profile into every instruction handler, which keeps the code small but still touches every step and loses speed. CPython's authors chose a third path: keep just one interpreter, but give it two dispatch tables. One points to the normal handlers; the other points every opcode to a single "recording" routine. Swapping between modes is a one-variable assignment — no branches at all.

That single recording routine is the clever bit. It captures whatever profiling data is needed, then hands execution straight back to the normal dispatch table for the real instruction. Think of it as a fan-in to one collector and a fan-out to the real work. The result, measured over 40 runs with CPU frequency scaling disabled, is a baseline interpreter at about 1.7 microseconds per loop and the instrumented version at 7.5 microseconds — roughly 4.5x overhead. Compare that with PyPy's meta-tracer, which has to record the interpreter itself and routinely pays 900 to 1,000 times the cost.

The payoff is JIT compilation that you can actually ship. CPython 3.15's JIT records the real instruction stream your program follows until a natural stopping point, then compiles that trace into machine code. Because the recording is cheap enough to leave on, the compiler gets rich, accurate data instead of guessing. The same dispatch-table swap could also power production profilers, type-profile collection, or any situation where you need to observe a running interpreter without stopping it. The honest trade-off, as the author notes, is complexity: the mechanism is elegant, but not simple to reason about.

Knowledge takeaway: CPython 3.15 profiles via a runtime dispatch-table swap, avoiding duplicated interpreters and branching flags; profiling costs under 5x speed versus 900x+ in older meta-tracers; the recorded traces feed the new JIT compiler so Python can compile hot code paths to machine code on the fly.