You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR RustPython#8350 (issue #42) removed the per-execution hash probes from the specialized LOAD_ATTR/STORE_ATTR handlers using dict keys-version stamps (shared per shape) and cached entry-index hints. That captured the guard sharing half of CPython's split keys: instances of the same class carry equal stamps, so one inline cache serves all of them.
What it did not capture is the structural half. Even on a full cache hit, the specialized load still pays, per access:
Dict::get_hint — inner RwLock read + entry identity check + value clone through a non-inlined call
In an attribute-read microbenchmark these two are ~6% of samples — essentially all of the remaining attr-specific cost. CPython's LOAD_ATTR_INSTANCE_VALUE pays none of it: after the type-version guard it reads obj->values[index] directly; no dict object, no lock, no probe.
Per-instance memory is also duplicated today: every instance dict owns a full Dict (indices table + key refs + hashes) even though all instances of a class share the same key set.
Proposal: CPython-style split keys
Give instances a "split" storage mode: a shared keys object per class (ordered interned keys + hash table, owned by the type or the shape registry) plus a per-instance values array indexed by insertion order.
LOAD_ATTR_INSTANCE_VALUE becomes: type-version guard → keys-version guard → values[cached_index] read. No dict object access at all.
STORE_ATTR on an existing key becomes an indexed write; appending a new key follows the shared-keys insertion order or falls back to un-split.
obj.__dict__ access materializes a real dict view lazily (or a dict-like proxy over keys+values), un-splitting on layout-diverging mutation — same lifecycle as CPython's PyDictOrValues.
Per-instance memory drops to one pointer + values array.
Un-split triggers and mechanics: deletion, non-interned key, layout divergence between instances, __dict__ replacement, exceeding the shared-keys capacity. The transition must be safe against concurrent readers.
Free-threading: values array reads/writes need a synchronization story. CPython 3.13t keeps split keys with per-object locking + optimistic lock-free reads; our objects have no per-object lock today, so this likely needs a seqlock or QSBR-style scheme consistent with the existing type-cache patterns.
__dict__ semantics: id stability, vars(obj) mutation coherence, dict views observing later attribute stores — CPython's materialization rules should be matched exactly.
Interaction with GC/traverse: values arrays must be traversed; materialized dicts must not double-count.
Expected effect
Attr-read overhead currently ~16ns vs CPython 2.2ns (M5 Max); the dict-access share (~6% of the loop) is what this removes, worth roughly 3–5ns/iter on that micro today.
Background
PR RustPython#8350 (issue #42) removed the per-execution hash probes from the specialized LOAD_ATTR/STORE_ATTR handlers using dict keys-version stamps (shared per shape) and cached entry-index hints. That captured the guard sharing half of CPython's split keys: instances of the same class carry equal stamps, so one inline cache serves all of them.
What it did not capture is the structural half. Even on a full cache hit, the specialized load still pays, per access:
owner.dict()—InstanceDictRwLock read +PyDictRefrefcount inc/decDict::get_hint— inner RwLock read + entry identity check + value clone through a non-inlined callIn an attribute-read microbenchmark these two are ~6% of samples — essentially all of the remaining attr-specific cost. CPython's
LOAD_ATTR_INSTANCE_VALUEpays none of it: after the type-version guard it readsobj->values[index]directly; no dict object, no lock, no probe.Per-instance memory is also duplicated today: every instance dict owns a full
Dict(indices table + key refs + hashes) even though all instances of a class share the same key set.Proposal: CPython-style split keys
Give instances a "split" storage mode: a shared keys object per class (ordered interned keys + hash table, owned by the type or the shape registry) plus a per-instance values array indexed by insertion order.
LOAD_ATTR_INSTANCE_VALUEbecomes: type-version guard → keys-version guard →values[cached_index]read. No dict object access at all.STORE_ATTRon an existing key becomes an indexed write; appending a new key follows the shared-keys insertion order or falls back to un-split.obj.__dict__access materializes a real dict view lazily (or a dict-like proxy over keys+values), un-splitting on layout-diverging mutation — same lifecycle as CPython'sPyDictOrValues.Design questions to settle first
PyType(heaptype ext, like CPython'sht_cached_keys) vs. keyed by shape in the existing shape registry from Use dict keys-version stamps and entry-index hints in LOAD_ATTR/STORE_ATTR specializations RustPython/RustPython#8350. Type-owned matches CPython's insertion-order-per-class semantics; shape-keyed shares across classes with identical__init__layouts but complicates growth.__dict__replacement, exceeding the shared-keys capacity. The transition must be safe against concurrent readers.__dict__semantics: id stability,vars(obj)mutation coherence, dict views observing later attribute stores — CPython's materialization rules should be matched exactly.Expected effect
Part of #38. Follow-up to #42 / RustPython#8350.
Researched and written by Claude on behalf of @youknowone. Part of the performance tracking series.