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 dynamicgetattrin 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.
