ShadowfetchAI written · under human editorial direction

Technology

Python 3.15’s JIT profiling trick makes interpreter instrumentation a design surface

CPython 3.15’s trace-recording JIT work matters less as a speed headline than as a sign that runtime observability is becoming an interpreter design surface.

AI Avatar
By Francesca Longness9 min read
Python 3.15’s JIT profiling trick makes interpreter instrumentation a design surface

CPython 3.15’s most interesting JIT change is not just that the JIT is getting faster; it is that the interpreter now has a low-overhead trace-recording path that treats instrumentation as something CPython can switch into, rather than bolt on.

That distinction matters if you build Python extensions, profilers, observability agents, coverage tools, debuggers, or runtime performance systems. The headline feature in the Python 3.15 documentation is an upgraded JIT compiler, with a new tracing frontend and measured prerelease speedups. The quieter design shift is the profiling mechanism Ken Jin described on July 1, 2026: CPython can enter a trace-recording mode by swapping dispatch behavior so bytecode execution fans through a recording instruction and then returns to the normal interpreter path.

In other words, CPython is not merely growing a faster execution tier. It is experimenting with a cheaper way to observe execution.

What happened

Ken Jin’s July 1 post describes work behind CPython 3.15’s JIT trace recording. Trace recording means CPython observes an actual execution path through Python bytecode, records that path, and hands the result to the JIT compiler. That requires an interpreter that can run while also collecting enough information for compilation.

The conventional choices are unattractive. One option is to maintain two interpreters: a normal interpreter and a profiling interpreter. Jin writes that this was CPython’s initial trace-recording implementation and that it worked better with the tail-calling interpreter than with the computed-goto interpreter. He gives the important figure: on pyperformance, the computed-goto approach produced roughly a 6% slowdown in that earlier two-interpreter design. Another option is to add a profiling mode guarded by a boolean branch inside the interpreter. That avoids duplicating the interpreter, but it means the hot path carries the profiling check even when profiling is usually off.

The 3.15 design described in the post takes a third route: dual dispatch. Interpreters commonly use a dispatch table to map each opcode to the code that implements it. CPython’s trace-recording trick is to switch the active dispatch table when tracing begins. In Jin’s simplified code excerpt, entering tracing assigns DISPATCH_TABLE_VAR = TRACING_DISPATCH_TABLE; leaving tracing assigns DISPATCH_TABLE_VAR = DISPATCH_TABLE.

The clever part is what the tracing dispatch table points to. A naive second table could become, effectively, another interpreter. Jin says that was exactly the problem to avoid. Instead, the second table maps instructions to a single recording/profiling instruction. That instruction records what it needs and then dispatches back through the ordinary non-tracing path for execution. Jin describes this as a fan-in, fan-out model: many bytecodes enter one tracing instruction, then execution continues through the normal interpreter.

The source backs up the shape of the design. In CPython’s current Python/bytecodes.c, the _JIT operation calls _PyJit_TryInitializeTracing(...) and then ENTER_TRACING() when tracing initialization succeeds. Later in the same file, inst(TRACE_RECORD, (--)) asserts that JIT tracing is active, calls _PyJit_translate_single_bytecode_to_trace(...), leaves tracing when the trace is full, clears previously recorded values, updates the tracer’s previous instruction state, reads record entries for the current opcode, and then dispatches back to the non-tracing path. Nearby comments label a group of tier-2 _RECORD_* operations as record ops for the JIT tracer and explicitly say they are NOPs for normal evaluation and only run during trace recording.

Python’s public 3.15 documentation describes the related JIT-facing outcome. In the Python 3.15.0b4 documentation, the “Upgraded JIT compiler” section says the JIT now has a new tracing frontend that records actual execution paths through code rather than estimating them as the previous implementation did. It also lists broader bytecode and control-flow support, basic register allocation, more JIT optimizations, unwinding support for GDB and GNU backtrace(), and better machine-code generation.

The release context is also important. PEP 790 says Python 3.15 is still in prerelease as of this slot: 3.15.0 beta 4 was scheduled for July 18, 2026, release candidate 1 is expected August 4, 2026, and final 3.15.0 is expected October 1, 2026. The implementation and results should be read as active 3.15 work, not as a promise that every internal detail is frozen for final.

The benchmark is useful, but not the whole story

Jin’s post includes a small overhead measurement for the profiling path. With dynamic frequency scaling turned off on his system, he reports medians of 40 runs for a toy benchmark: no profiling at 1.72e-06s, and interpreter plus profiling plus JIT compilation at 7.47e-06s. He summarizes that as at most 4.5x slower on the toy benchmark.

That number is not a general-purpose profiler benchmark. The post says it is a toy benchmark, and it includes the test script in an appendix. Jin also contrasts it with PyPy tracing slowdowns in the 900x–1000x range, but he explicitly warns that the comparison is not fair because PyPy is meta-tracing and therefore traces a lot more code for the same program. That caution is the right way to read the data: the precise ratio is less important than the direction of the design.

The interesting property is not that tracing is free. It is not. The interesting property is that CPython is trying to keep the normal interpreter path clean while providing a specialized, low-overhead route for the JIT to capture execution. That is a systems-design choice, not a benchmark brag.

Why extension authors should care

Most extension authors do not need to implement a JIT. Many will never touch CPython’s internal tracing machinery. But they do need to understand the direction CPython is moving: runtime behavior is becoming more tiered, more profile-guided, and more sensitive to the distinction between normal execution, monitoring, and JIT recording.

If you maintain a C extension, this should change what you assume about “the interpreter.” There is still one user-visible Python language, but internally the execution path can include adaptive bytecode, tier-2 operations, tracing states, JIT executors, instrumentation checks, and dispatch behavior that changes by mode. Code that relies on undocumented interpreter internals becomes riskier in that world. Code that sticks to documented C APIs, stable ABI where appropriate, and explicit compatibility testing across prereleases becomes more valuable.

For extension maintainers, the practical action is boring and important: test against Python 3.15 betas and release candidates before final. Watch for assumptions about frame layout, opcode names, tracing behavior, recursion, and error handling. If your project patches CPython internals, inspects bytecode at a low level, or depends on profiling hooks, treat 3.15 as a compatibility cycle rather than a routine rebuild.

The same applies to tools that instrument Python code. If your profiler, tracer, coverage tool, APM agent, debugger, or sandbox depends on what happens around frames and bytecodes, you should map how your hooks interact with Python 3.15’s monitoring and JIT paths. The fact that CPython’s JIT trace recorder uses its own low-overhead route does not mean that route is a public plugin surface. It does mean the core runtime is investing in mode-specific observability. Tool builders should expect sharper boundaries between public monitoring APIs and internal execution recording.

Why performance-tool builders should care even more

The bigger opportunity is conceptual. Python observability has long been haunted by cost. Detailed tracing is powerful but expensive. Sampling is cheap but incomplete. Coverage is useful but can distort the program being measured. Debug hooks are necessary but not designed as high-frequency telemetry. When instrumentation costs too much, teams either turn it off in production or accept a coarser picture than they actually want.

CPython 3.15’s trace-recording design does not solve that problem for everyone. It is not a new public tracing API for vendors to drop into production. But it demonstrates an implementation pattern that performance tools should study: keep the default interpreter hot path as undisturbed as possible; enter an observation mode only when needed; funnel recording through a narrow instruction path; then return to ordinary dispatch.

That is different from treating observability as a layer outside the runtime. It treats observability as an execution-mode problem. If that idea matures, the future Python tooling conversation could move from “how much slowdown will tracing impose?” toward “which runtime surface should this observation use, and when should it be active?”

The Python 3.15 documentation already points in that direction in a different way. Its release highlights include PEP 799’s profiling package and Tachyon high-frequency statistical sampling profiler, and PEP 831’s frame pointers enabled by default for better system-level observability. Those are public-facing observability improvements. The JIT trace recorder is internal machinery. Together, they show a runtime that is paying attention to the cost and shape of measurement.

What practitioners should do now

First, separate three questions that are easy to blur. Is the Python 3.15 JIT faster? The draft 3.15 documentation reports prerelease pyperformance improvements for JIT builds over interpreter baselines, but also says the results are not final. Is the new trace-recording approach cheap enough for the JIT’s needs? Jin’s toy benchmark suggests the overhead is low enough to be interesting, while clearly not a production-wide guarantee. Is this a public observability API? No. The source and post describe CPython internals for JIT tracing, not a vendor contract.

Second, use the prerelease window. PEP 790 puts final Python 3.15 in October 2026, with release candidates expected before that. Projects with C extensions, debuggers, profilers, bytecode tooling, or runtime patching should test against 3.15 now, file precise bugs upstream when behavior changes unexpectedly, and avoid waiting for final builds to discover that an internal assumption broke.

Third, design your own instrumentation with mode switching in mind. Even if you never touch CPython internals, the pattern is portable: do not make every request pay for data you only need during investigation; keep the normal path simple; define explicit entry and exit conditions for high-detail recording; make the recorded data useful enough to justify the cost; and document the overhead honestly.

Fourth, avoid marketing your Python tool around internal 3.15 behavior unless you can prove it through supported APIs. The presence of TRACE_RECORD in CPython source is evidence of how the core JIT tracer works. It is not permission to depend on private symbols. The right product move is to track the direction, not to couple your users to an implementation detail that could change before or after 3.15 final.

The JIT headline will get the attention because speedups are easy to package. The deeper lesson is that CPython’s maintainers are shaping the interpreter so measurement can have a more deliberate place in execution. For people who build performance tools, that is the more important story: instrumentation is becoming a design surface, and Python’s cost model for observability may not stay where it has been.


Shadowfetch is an independent software company publishing evidence-based journalism. Explore Shadowfetch Linux — our own Linux build — and the Shadowfetch apps on the App Store.

AI written · under human editorial direction

Sources

Drawn from Ken Jin’s July 1 post, CPython bytecodes.c source, Python 3.15 documentation, and PEP 790.

Evidence types: blog post, source code, documentation, PEP

Links verified

See a problem in this story? Report an error · Corrections policy · Our methodology

The Daily Newsletter

One morning email: the day’s biggest stories — politics, world, business, science and culture.

Double opt-in. Unsubscribe anytime. See our privacy policy.