Adding a Bytecode Specialization

CPython uses an adaptive, specializing interpreter (PEP 659) to optimize bytecode execution dynamically at runtime. When generic opcodes (such as CONTAINS_OP or BINARY_OP) execute frequently with predictable operand types, CPython morphs them into specialized fast-path micro-ops (uops) to bypass generic type dispatch.

This guide describes the step-by-step process of introducing a new bytecode specialization in CPython.

Note

This guide uses the specialization of CONTAINS_OP (from CPython PR #116385) as a reference example.

Overview of Steps

Adding a new bytecode specialization involves coordinated changes across several files in the CPython source tree:

  1. 1. Modify the Opcode Definition in Python/bytecodes.c

  2. 2. Add the Specializing Micro-Op (uop)

  3. 3. Define the Macro Instruction

  4. 4. Define the Cache Structure in Include/internal/pycore_code.h

  5. 5. Write the Specializing Function in Python/specialize.c

  6. 6. Update Operation Statistics in Python/specialize.c

  7. 7. Add Cache Layout in Lib/opcode.py

  8. 8. Bump the Magic Number in Include/internal/pycore_magic_number.h

  9. 9. Regenerate Code Files

1. Modify the Opcode Definition in Python/bytecodes.c

In Python/bytecodes.c, locate the existing generic instruction. Change its definition from a top-level instruction (inst) to a micro-op (op) and prefix its name with an underscore.

For example, convert CONTAINS_OP into _CONTAINS_OP:

op(_CONTAINS_OP, (left, right -- res)) {
    // Implementation of generic operation
    int res_val = PySequence_Contains(right, left);
    if (res_val < 0) goto error;
    res = res_val ? Py_True : Py_False;
}

2. Add the Specializing Micro-Op (uop)

Add a new uop definition in Python/bytecodes.c that triggers the specialization check when the execution counter reaches zero.

op(_SPECIALIZE_CONTAINS_OP, (left, right -- left, right)) {
    _PySpecializer_Requestation(this_instr, _SPECIALIZE_CONTAINS_OP);
}

3. Define the Macro Instruction

Combine the specializing uop and the original uop into a macro instruction using macro syntax in Python/bytecodes.c:

macro(CONTAINS_OP) = _SPECIALIZE_CONTAINS_OP + _CONTAINS_OP;

4. Define the Cache Structure in Include/internal/pycore_code.h

Define a C struct for the instruction’s inline cache in Include/internal/pycore_code.h. Every cache entry must include at least a 16-bit specialization counter (counter), plus any specialized metadata or version pointers needed.

typedef struct {
    _PySpecializationCacheTop counter;
    // Additional cache fields if needed (e.g., version or type pointers)
} _PyContainsOpCache;

5. Write the Specializing Function in Python/specialize.c

Implement the specializing logic in Python/specialize.c. This function inspects the runtime operand types, checks whether they qualify for a fast path, updates the cache, and rewrites the opcode if appropriate.

void
_Py_Specialize_ContainsOp(PyObject *left, PyObject *right, _Py_CODEUNIT *instr)
{
    _PyContainsOpCache *cache = (_PyContainsOpCache *)instr;
    if (PySet_CheckExact(right)) {
        // Specialize for set containment
        instr->op.code = _BINARY_OP_CONTAINS_SET;
    }
    else {
        // Fallback / UNSTATISFIED
        STAT_INC(CONTAINS_OP, failure);
    }
}

6. Update Operation Statistics in Python/specialize.c

Track specialization hits, misses, and execution counts by calling add_stat_dict() or incrementing statistic counters in Python/specialize.c:

STAT_INC(CONTAINS_OP, hit);

7. Add Cache Layout in Lib/opcode.py

Update Lib/opcode.py to inform Python’s dis (disassembler) module about the size and structure of the new instruction’s inline cache entries:

_specialized_opcodes["CONTAINS_OP"] = {
    "counter": 1,
}

8. Bump the Magic Number in Include/internal/pycore_magic_number.h

Because adding or altering inline cache structures changes the bytecode format, increment MAGIC_NUMBER in Include/internal/pycore_magic_number.h. This ensures older .pyc files are invalidated and recompiled.

9. Regenerate Code Files

Run the code generators to update auto-generated files (like opcode_targets.h, executor_cases.c, etc.):

On Linux / macOS:

make regen-all

On Windows:

build.bat --regen

See Also