CPython Specialize Stats: Why Hot Functions Stop Adaptive Instructions

CPython Specialize Stats: Why Hot Functions Stop Adaptive Instructions

CPython 3.11+ specializes bytecodes (load attr, binary ops, calls). A “harmless” refactor that makes a call site megamorphic silently unspecializes the hot path and your p99 regresses with no code-size change. The unfair advantage is reading specialization stats instead of guessing.

⚡ TL;DR: Run with PYTHON_STATS=1 / _opcode.get_specialization_stats() on 3.12+; compare before/after on the hot function; keep attribute and call sites monomorphic; avoid __getattr__ and dynamic getattr in inner loops. Pair with V8 Maglev vs TurboFan and Python GIL Contention.

See the specialization die

# ✅ Micro-harness around a hot function (CPython 3.12+)
import collections
import dis
import sys

def hot(xs, key):
    s = 0
    for x in xs:
        s += getattr(x, key)  # ❌ often kills specialization
    return s

class Row:
    __slots__ = ("a",)
    def __init__(self, a): self.a = a

rows = [Row(i) for i in range(100_000)]

# Warm
for _ in range(5):
    hot(rows, "a")

# Inspect bytecode adaptive forms
dis.dis(hot, adaptive=True)

if hasattr(sys, "_stats_on"):
    # enable via PYTHON_STATS=1 when building/running specialized builds
    pass
# ✅ Prefer monomorphic attribute access
def hot_fast(xs):
    s = 0
    for x in xs:
        s += x.a  # ✅ stable LOAD_ATTR specialization
    return s

What breaks specialization

Change Effect on adaptive ops
getattr(obj, name) with varying name Miss / deopt
Duck types alternating classes Megamorphic attr/call
Adding __getattr__ / properties Side exits
Wrapping in logging decorator mid-loop Call site pollution
Storing unbound methods differently CALL deopt
# ❌ “Clean” refactor that regresses p99
def score(items):
    return sum(getattr(i, field) for i in items for field in ("a", "b", "c"))
# ✅ Three monomorphic loops or explicit fields
def score(items):
    s = 0
    for i in items:
        s += i.a + i.b + i.c
    return s

Production discipline

Benchmark the exact function under py-spy / perf before blaming the network. If specialization stats show high miss rates on LOAD_ATTR/CALL, fix shapes first—not instance size.

Closing checklist

✅ Dos
– ✅ Keep hot call/attr sites monomorphic
– ✅ Use __slots__ / stable types in inner loops
– ✅ Diff dis.dis(..., adaptive=True) across refactors
– ✅ Gate performance PRs on p99 + specialization miss rate
– ✅ Treat dynamic getattr as a smell on hot paths

❌ Don’ts
– ❌ Don’t “simplify” hot loops with reflective access
– ❌ Don’t ignore p99 regressions under 5% code diffs
– ❌ Don’t mix many duck types in one call site
– ❌ Don’t enable stats forever in prod (overhead)

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply