Status update (2026-07-22)
The specialization blocker tracked in #41 is fixed — LOAD_ATTR now specializes for plain user classes — so the probes described here are no longer hypothetical: they are the active ceiling on attribute-heavy code. Verified at dd9bce5 that all three findings below still hold (line numbers refreshed).
Fresh profile evidence (Apple M5 Max, release build; see #41 for methodology): in a function-scoped s += c.x loop, Dict::get<PyInterned<PyStr>> inside LoadAttrInstanceValue alone is ~7% of samples, and overall attribute-read overhead is still ~11.5x CPython 3.14 (25.4ns vs 2.2ns).
Problem
Even when LOAD_ATTR does specialize, the specialized handlers still perform a full dict hash-probe per execution, where CPython's equivalents are a couple of integer compares.
Causes
crates/vm/src/frame.rs, specialized handlers (line numbers at dd9bce5):
-
LoadAttrWithHint (~4481) ignores its hint. It runs dict.get_item_opt(attr_name, vm) — a complete hash + probe — every time. The dict already supports exactly the needed API: LoadGlobalModule uses get_item_opt_hint(name, cached_index, vm) (~6319) with a cached entry index + dict version, skipping the probe on hit. The instance-attr path never got the same treatment — the specializer (~8432–8438) writes only the type version into the cache, no entry index.
-
LoadAttrMethodWithValues (~4424) does a full dict probe as its shadow check. To verify the method isn't shadowed by an instance attribute it runs dict.get_item_opt(attr_name, vm) on the instance dict per call (~4434–4445). CPython's LOAD_ATTR_METHOD_WITH_VALUES compares the cached dict keys version (a single u32) — possible because unmodified instances of the same class share a keys object. RustPython dicts have a version() counter (dict_inner.rs ~266) that could be cached per-site: if dict.version() == cached_version { not shadowed }, falling back to the probe only when the dict changed.
-
LoadAttrInstanceValue (~4459) likewise re-hashes the name each time. There is no split-keys/managed-dict layout, so "read attribute" can't be "load values[offset]" like CPython's LOAD_ATTR_INSTANCE_VALUE; at minimum it can use the hint mechanism from (1).
-
StoreAttrInstanceValue/StoreAttrWithHint (~4714–4751) have the same shape: type-version guard, then a full dict.set_item hash + probe per execution.
Suggested direction
Short term (no dict redesign):
- Store an entry-index hint + dict version in the LOAD_ATTR inline cache (there are 9 cache slots) and use
get_item_opt_hint in LoadAttrWithHint/LoadAttrInstanceValue.
- Replace the
MethodWithValues shadow probe with a cached dict-version compare, re-probing only on version mismatch.
- Same idea applies to
StoreAttr* handlers.
Longer term:
- CPython-style split keys (shared keys object per class + per-instance values array) would make instance attr access a version check + indexed load, and would also shrink per-instance memory. This is a big change to
dictdatatype/object layout and interacts with free-threading (CPython 3.13t keeps split keys with per-object locking), so it deserves its own design pass after the short-term wins land.
Researched and written by Claude on behalf of @youknowone. Part of the performance tracking series.
Part of #38.
Status update (2026-07-22)
The specialization blocker tracked in #41 is fixed — LOAD_ATTR now specializes for plain user classes — so the probes described here are no longer hypothetical: they are the active ceiling on attribute-heavy code. Verified at dd9bce5 that all three findings below still hold (line numbers refreshed).
Fresh profile evidence (Apple M5 Max, release build; see #41 for methodology): in a function-scoped
s += c.xloop,Dict::get<PyInterned<PyStr>>insideLoadAttrInstanceValuealone is ~7% of samples, and overall attribute-read overhead is still ~11.5x CPython 3.14 (25.4ns vs 2.2ns).Problem
Even when LOAD_ATTR does specialize, the specialized handlers still perform a full dict hash-probe per execution, where CPython's equivalents are a couple of integer compares.
Causes
crates/vm/src/frame.rs, specialized handlers (line numbers at dd9bce5):LoadAttrWithHint(~4481) ignores its hint. It runsdict.get_item_opt(attr_name, vm)— a complete hash + probe — every time. The dict already supports exactly the needed API:LoadGlobalModuleusesget_item_opt_hint(name, cached_index, vm)(~6319) with a cached entry index + dict version, skipping the probe on hit. The instance-attr path never got the same treatment — the specializer (~8432–8438) writes only the type version into the cache, no entry index.LoadAttrMethodWithValues(~4424) does a full dict probe as its shadow check. To verify the method isn't shadowed by an instance attribute it runsdict.get_item_opt(attr_name, vm)on the instance dict per call (~4434–4445). CPython'sLOAD_ATTR_METHOD_WITH_VALUEScompares the cached dict keys version (a single u32) — possible because unmodified instances of the same class share a keys object. RustPython dicts have aversion()counter (dict_inner.rs~266) that could be cached per-site:if dict.version() == cached_version { not shadowed }, falling back to the probe only when the dict changed.LoadAttrInstanceValue(~4459) likewise re-hashes the name each time. There is no split-keys/managed-dict layout, so "read attribute" can't be "load values[offset]" like CPython'sLOAD_ATTR_INSTANCE_VALUE; at minimum it can use the hint mechanism from (1).StoreAttrInstanceValue/StoreAttrWithHint(~4714–4751) have the same shape: type-version guard, then a fulldict.set_itemhash + probe per execution.Suggested direction
Short term (no dict redesign):
get_item_opt_hintinLoadAttrWithHint/LoadAttrInstanceValue.MethodWithValuesshadow probe with a cached dict-version compare, re-probing only on version mismatch.StoreAttr*handlers.Longer term:
dictdatatype/object layout and interacts with free-threading (CPython 3.13t keeps split keys with per-object locking), so it deserves its own design pass after the short-term wins land.Researched and written by Claude on behalf of @youknowone. Part of the performance tracking series.
Part of #38.