Python 3.15's JIT: What It Actually Took

CPython core developer Ken Jin documents how the JIT went from producing no speedups in 3.13 to 11-12% faster on macOS AArch64 in 3.15, through trace recording, reference-count elimination, and deliberate community design.

Python 3.15's JIT: What It Actually Took

The CPython JIT shipped in 3.13 was slower than the interpreter it was supposed to accelerate. The team responsible for it lost its main sponsor a few months later. Ken Jin’s account of what happened between that low point and a genuine 11-12% gain in 3.15 is less about algorithms than about how open-source infrastructure projects actually move.

Source

Python 3.15’s JIT is now back on track

By: Ken Jin (CPython core developer)
Published: Mar 17 2026
Type: Engineering article

I saved this because it documents the JIT’s actual multi-year trajectory rather than any single launch-day headline, and the sections on trace recording and reference-count elimination are among the clearest short explanations of what the 3.15 optimizer actually does differently from 3.13.

What it actually took

The headline numbers are 11-12% faster than the tail-calling interpreter on macOS AArch64, and 5-6% faster than the standard interpreter on x86_64 Linux, measured as geometric means across the pyperformance suite. The range within that mean is wide — a 20% slowdown to over 100% speedup depending on the benchmark — which is normal for JIT optimizations that benefit loops and type-stable code while adding overhead to code that never gets warm enough to trigger them. Free-threading support is still pending, targeted for 3.15 or 3.16.

The 3.13 JIT and 3.14 JIT were effectively neutral or negative on performance, as Jin documented in an earlier post. The Faster CPython team lost its main funding sponsor in 2025, which didn’t affect Jin as a volunteer but did affect colleagues, and at that point the project’s future was genuinely uncertain. Jin’s response was to frame the JIT as a community stewardship project rather than a corporate deliverable. The core sprint in Cambridge produced a written plan: 5% faster by 3.15, 10% by 3.16, free-threading support, and at least two active maintainers at each of the three JIT stages — frontend, middle-end, backend. Reducing the bus factor was treated as equally important as the performance target.

The technical breakthrough that moved the numbers was trace recording. Jin rewrote the JIT frontend to record sequences of instructions as they actually execute — tracing live paths through the interpreter — rather than operating on the static bytecode as written. The mechanism, which Jin calls dual dispatch, uses two dispatch tables: the normal interpreter table, and a second table where every entry points to a single tracing instruction. Having only one instruction responsible for recording traces rather than tracing versions of all instructions means the interpreter grows by exactly one instruction, not by a factor proportional to opcode count. A doubled interpreter causes compiled code bloat that erases the benefit; the single-instruction approach keeps the base interpreter fast. The trace recording pass itself runs 3-5x slower than the specializing interpreter, but it only runs when a trace is being built, not on every call.

The impact was roughly 50% more JIT code coverage. Execution paths through conditionals, rare branches, and cross-basic-block sequences that the static-bytecode approach never reached became reachable. That coverage gain compounded every later optimization that landed on top of it.

The second major optimization was reference-count elimination. Every Python object carries a reference count, and every DECREF involves at least one branch — the check for whether the count reached zero and deallocation is needed. Jin found that even after bytecode-level reference-count optimization, each DECREF still emitted a branch in JIT output. Eliminating the branch is inexpensive to express and requires proving that the decrement cannot trigger deallocation. For a large subset of instructions this proof is essentially mechanical, which made it straightforward to parallelize across contributors. Jin directed new JIT contributors to that specific issue, with detailed instructions on exactly what to change and which instructions to tackle. Eleven contributors worked on it, several with no prior JIT experience. The core of the JIT went from an opaque blob requiring deep expertise to something a C programmer could contribute to by following clear units of work.

Jin is honest that luck was as important as design. The dual dispatch mechanism emerged from misreading a suggestion by Mark Shannon — he intended something narrower, and the misunderstanding produced a better outcome. The trace recording prototype took three days to write and a month to pass the full test suite. The performance went from 6% slower than the interpreter to 1-2% faster over weeks of iteration on edge cases, not from a single insight.

The infrastructure supporting daily visibility into these numbers is four machines in Savannah Ostrowski’s closet. Daily JIT performance runs turned monthly uncertainty into daily feedback, which shortened the loop between “does this optimization help” and “yes, commit it.”

What I want to remember

Remember: A JIT that ships with zero visible gains isn’t a failed JIT — the 3.13 copy-and-patch layer was the necessary foundation; trace recording was the coverage multiplier that made later work meaningful; reference-count elimination was the instruction-level change that turned coverage into actual measured speed.