logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·01.11.26
← Back to news

Python 3.15's Ultra-Low Overhead Interpreter Profiling Mode – Ken Jin's Blog

Original article
fidget-spinner.github.io|119 points|8 comments|by rbanffy|Jul 15, 2026

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:

  1. Record a stream of instructions during actual program runtime.
  2. Continue recording until a specific termination condition is met.
  3. 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 ≈6%\approx 6\%≈6% 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 if statements.
  • 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:

  1. Fan-In: Any opcode encountered while in profiling mode leads to the same profiling handler.
  2. Profiling: This handler records the necessary data.
  3. 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.

ModeMedian Execution Time
Standard Interpreter (No Profiling)1.72×10−6s1.72 \times 10^{-6}\text{s}1.72×10−6s
Interpreter + Profiling + JIT Compilation7.47×10−6s7.47 \times 10^{-6}\text{s}7.47×10−6s

Analysis: In this toy benchmark, profiling the interpreter is at most ≈4.5x\approx 4.5\text{x}≈4.5x slower.

Comparison Note: Other systems like PyPy can see slowdowns of 900x900\text{x}900x to 1000x1000\text{x}1000x. 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.

Conceptual Diagram of JIT Flow


📎 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