Skip to main content

specialize.c: Adaptive Specialization

specialize.c implements CPython's adaptive interpreter. When an instruction is executed enough times, the runtime rewrites it in-place with a faster specialized variant. In 3.14, successful specializations can also be promoted into tier-2 micro-operations (uops) for the optimizer.

Map

LinesSymbolRole
1–120_PyAdaptiveCounterBackoff counter embedded in each cache entry; decrements on each miss and triggers re-specialization at zero
121–350_Py_Specialize_LoadAttrMain entry point for LOAD_ATTR_ADAPTIVE; probes type version tag and selects a slot variant
351–600_Py_Specialize_StoreAttrMirror of LoadAttr for stores; guards against descriptors that define __set__
601–900_Py_Specialize_BinaryOpNumeric fast path; checks both operand types and rewrites to BINARY_OP_ADD_INT, _FLOAT, or _UNICODE
901–1100_Py_Specialize_CompareOpSelects COMPARE_OP_INT / _FLOAT / _STR
1101–1400_Py_Specialize_CallHandles CALL_ADAPTIVE; covers PyCFunction, bound methods, and Python callables
1401–1700_Py_Specialize_SubscribeBINARY_SUBSCR variants: list index, dict key, and tuple index
1701–2000specialize_class_loadHelper shared by LoadAttr and LoadSuper; validates tp_version_tag
2001–2400_Py_Specialization_StatsCompile-time stats counters (enabled with SPECIALIZATION_STATS)
2401–3000uop promotion helpers_PyOptimizer_Optimize glue that feeds a hot trace into the tier-2 compiler

Reading

_PyAdaptiveCounter backoff

Every inline cache slot contains a small counter. On a specialization miss the counter is decremented. When it reaches zero the runtime calls the appropriate _Py_Specialize_* function to attempt re-specialization.

/* Python/specialize.c:42 _PyAdaptiveCounter_Trigger */
static inline int
_PyAdaptiveCounter_Trigger(_PyAdaptiveCounter *counter)
{
counter->value -= (1 << ADAPTIVE_BACKOFF_BITS);
return counter->value <= 0;
}

The macro ADAPTIVE_BACKOFF_BITS (currently 3) gives eight misses before the next re-specialization attempt. This prevents thrashing on polymorphic call sites.

_Py_Specialize_LoadAttr cache probe

/* Python/specialize.c:180 _Py_Specialize_LoadAttr */
int
_Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
{
PyTypeObject *type = Py_TYPE(owner);
if (type->tp_dict == NULL) {
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_NO_DICT);
goto fail;
}
...
instr->op.code = LOAD_ATTR_SLOT;
return 0;
fail:
STAT_INC(LOAD_ATTR, failure);
instr->op.code = LOAD_ATTR;
return 0;
}

The function never raises; it silently falls back to the generic opcode so the interpreter can continue.

Tier-2 uop promotion

Once a trace is hot enough, _PyOptimizer_Optimize replaces a RESUME with a ENTER_EXECUTOR opcode that points at a compiled _PyExecutorObject. The specialization side-channel feeds type-version information into the uop compiler so guards can be omitted when types are proven stable.

/* Python/specialize.c:2450 maybe_promote_to_tier2 */
static void
maybe_promote_to_tier2(_PyInterpreterFrame *frame, _Py_CODEUNIT *instr)
{
if (_Py_IsMainInterpreter(_PyInterpreterState_GET())) {
_PyOptimizer_Optimize(frame, instr, NULL);
}
}

gopy notes

  • The adaptive counter lives in objects/object.go as AdaptiveCounter.
  • _Py_Specialize_LoadAttr logic is partially mirrored in compile/flowgraph_passes.go; the type-version tag check is omitted because gopy resolves attribute slots at compile time.
  • BinaryOp specialization is not yet ported. The fallback generic arithmetic path in vm/eval_gen.go handles all numeric types today.
  • Uop promotion glue is tracked under v0.12 scope (task #482).