Python 3.15's Ultra-Low Overhead Interpreter Profiling Mode – Ken Jin's Blog
Python 3.15's Ultra-Low Overhead Interpreter Profiling Mode
Adapted from Ken Jin's Blog — July 1, 2026
The JIT compiler introduced in Python 3.15 offers modest performance gains over the standard interpreter. A critical component enabling this is a novel approach to interpreter profiling designed specifically to record execution paths for JIT compilation.
This method is significant because it maintains extremely low overhead, ensuring that the process of preparing for JIT compilation doesn't negate the speed benefits of the JIT itself. While similar concepts might exist elsewhere, the author notes that this specific implementation isn't widely documented.
🔍 Background: Trace Recording
As previously discussed in earlier posts, the CPython 3.15 JIT relies on trace recording. The workflow is as follows:
- Record a stream of instructions during actual program runtime.
- Continue recording until a specific termination condition is met.
- Pass that recorded stream to the JIT compiler.
To achieve this, the interpreter must be instrumented or "profiled."
🛠️ The Quest for Efficient Profiling
There are generally two traditional ways to profile an interpreter, but both have significant drawbacks:
Approach 1: The Dual-Interpreter Model
Create two distinct interpreters: one for standard execution and one dedicated to profiling.
- Mechanism: The main interpreter calls into the profiling interpreter and then returns.
- The Flaw: While acceptable for tail-calling interpreters, it was sluggish for computed goto interpreters (causing a slowdown in
pyperformance). - Reason: This effectively doubles the size of the C binary for the interpreter (since the feature is function-scoped), which harms cache performance and overall speed.
Approach 2: The Conditional "Profiling Mode"
Use a boolean flag (e.g., bool profile) to toggle profiling logic.
- Mechanism: Wrap profiling code in
ifstatements. - The Flaw: Even though CPUs are great at branch prediction, this approach was deemed too disruptive to the interpreter's core execution flow.
🚀 The Solution: Dual Dispatch
To combine the efficiency of a single interpreter with the flexibility of a profiling mode, the team implemented Dual Dispatch.
Most interpreters find the next instruction by mapping an opcode (an ID) to a specific function pointer or label address via a dispatch table. Instead of one table, Python 3.15 uses two.
The "Fan-In / Fan-Out" Model
A naive swap of tables would still feel like having two separate interpreters. The breakthrough was to map every single instruction in the profiling table to one singular recording instruction.
The logic flow works like this:
- Fan-In: Any opcode encountered while in profiling mode leads to the same profiling handler.
- Profiling: This handler records the necessary data.
- Fan-Out: The handler then uses the original dispatch table to jump to the actual execution logic for that instruction.
Implementation in C
Switching modes is as simple as updating a local variable. In the CPython 3.15 source, this is handled via macros:
#define ENTER_TRACING() \
DISPATCH_TABLE_VAR = TRACING_DISPATCH_TABLE;
#define LEAVE_TRACING() \
DISPATCH_TABLE_VAR = DISPATCH_TABLE;
(Note: These macros abstract the differences between computed goto and tail-calling interpreters.)
📊 Performance Results
To test this, the author disabled dynamic frequency scaling and ran a benchmark script 40 times.
| Mode | Median Execution Time |
|---|---|
| Standard Interpreter (No Profiling) | |
| Interpreter + Profiling + JIT Compilation |
Analysis: In this toy benchmark, profiling the interpreter is at most slower.
Comparison Note: Other systems like PyPy can see slowdowns of to . While this isn't a "fair" comparison—PyPy uses meta-tracing and must trace the interpreter itself—it highlights how potentially expensive tracing can be.
💭 Final Reflections
The author expresses pride in this system, noting that it could be adapted for other uses, such as:
- Low-overhead interpreter profiling without full rewrites.
- Recording runtime type profiles.
However, there is a lingering question regarding the trade-off:
"Is this magical system... worth the complexity?"
While the solution is elegant, it is not simple to reason about, and the author values simplicity in system design.
📎 Appendix: Testing the JIT
To trigger the JIT compilation in this environment, set the environment variable PYTHON_JIT_RESUME_INITIAL_VALUE=1 and run a tight loop:
def foo(x):
# A long sequence of operations to trigger the trace recorder
x + x
x + x
x + x
x + x
x + x
x + x
x + x
x + x
x + x
x + x
# ... (repeated many times)
x + x