Use dict keys-version stamps and entry-index hints in LOAD_ATTR/STORE_ATTR specializations#8350
Conversation
…tions Add a keys-version stamp to Dict: a globally unique u32 assigned lazily and reset on any key-set change (new key, deletion, clear). Value-only updates keep the stamp. - LoadAttrMethodWithValues / LoadAttrNondescriptorWithValues: cache the instance dict's stamp in the pointer cache and skip the shadow probe while the stamp matches. - LoadAttrWithHint: cache the entry index at specialization time; a hit is an identity check on the entry key instead of a hash probe, with self-refresh on miss. - StoreAttrWithHint / StoreAttrInstanceValue: replace values through the cached entry index via set_item_with_hint. Fixes #42 Assisted-by: Claude
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds dictionary key-version stamps and entry-index hints, integrates them into VM attribute load/store specialization, updates cache-pointer safety documentation, and adds regression tests for shadowing, key churn, shared shapes, dictionary replacement, and holey dictionaries. ChangesDictionary Attribute Caching
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AttributeCode
participant Frame
participant PyDict
participant DictStorage
AttributeCode->>Frame: execute specialized attribute operation
Frame->>PyDict: check keys_version and cached hint
PyDict->>DictStorage: get_with_hint or insert_with_hint
DictStorage-->>PyDict: value or updated entry hint
PyDict-->>Frame: lookup or store result
Frame-->>AttributeCode: continue fast path or deoptimize
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR improves RustPython’s adaptive attribute specialization by avoiding repeated full dict hash+probe lookups in specialized LOAD_ATTR* / STORE_ATTR* handlers. It introduces (1) a dict keys-version stamping mechanism for cheap “key set unchanged” guards and (2) entry-index hints to enable identity-checked fast paths for instance-dict loads/stores.
Changes:
- Add a globally-unique per-dict keys-version stamp (
keys_version) that is invalidated on key-set changes and can be used as a fast guard to skip repeated “shadow check” probes. - Add entry-index hint plumbing (
get_with_hint,insert_with_hint,get_item_opt_refresh_hint,set_item_with_hint) and use it inLOAD_ATTR_WITH_HINTandSTORE_ATTR_*to avoid hash+probe on hits. - Add regression coverage in
extra_tests/snippets/vm_specialization.pyfor post-warmup shadowing, value-only updates, and hint invalidation across churn /__dict__replacement /clear().
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| extra_tests/snippets/vm_specialization.py | Adds regression tests for new specialization fast-path guards and hint invalidation scenarios. |
| crates/vm/src/frame.rs | Switches specialized LOAD_ATTR/STORE_ATTR handlers to use keys-version shadow guards and entry-index hints; adds helper functions for hinted stores and shadow checks. |
| crates/vm/src/dict_inner.rs | Introduces keys_version stamping, invalidation on key-set changes, and new hint-based get/insert helpers returning entry indices. |
| crates/vm/src/builtins/dict.rs | Exposes hinted get/store APIs on PyDict plus accessors for keys-version stamping (exact dict only for assignment). |
| crates/compiler-core/src/bytecode.rs | Clarifies pointer-cache slot usage for non-pointer, tear-free guard values like keys-version stamps. |
Comments suppressed due to low confidence (1)
crates/vm/src/dict_inner.rs:957
pop_backcallsinvalidate_keys_version()only after it has already removed the live entry frominner.entriesviapop(). This violates the function’s own contract that the keys-version stamp must be invalidated before any key-set change to keep lock-free readers from observing a stale nonzero stamp after the key set has changed.
if let Some(entry) = entry {
break entry;
}
};
self.invalidate_keys_version();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// since before the probe. This is safe against concurrent key-set | ||
| /// changes because their reset trails the mutation (see | ||
| /// `bump_version_keys_changed`): a stamp installed during a change is | ||
| /// retired by that trailing reset before it could ever match again. |
There was a problem hiding this comment.
@fanninpm Yes — Copilot’s comment is correct.
bump_version_keys_changed does not exist in this file, and the assign_keys_version() documentation says invalidation “trails the mutation,” while the actual key-set mutation paths call invalidate_keys_version() before changing the dictionary under its write lock.
The comment should be updated to describe the pre-mutation invalidation protocol (or simply point to invalidate_keys_version) rather than the nonexistent trailing-reset helper.
There was a problem hiding this comment.
This was valid against the first commit, but the stale doc ("trails the mutation", bump_version_keys_changed) was already rewritten in 33b55c7: assign_keys_version now documents the shape-stamp derivation, and invalidate_keys_version's doc describes the actual pre-mutation reset protocol. The remaining feedback (clippy manual_hash_one, set_item_with_hint doc, single-cache-site snippet test) is addressed in 4f04de4.
— commented by Claude
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| /// Store using a cached entry index hint for the value-replace fast path. | ||
| /// | ||
| /// Returns a refreshed hint for the key when the hinted slot was vacant | ||
| /// (`None` otherwise). |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/vm/src/dict_inner.rs (1)
949-965: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
pop_backinvalidates the keys-version stamp after mutating the key set — reintroduces the exact race the new protocol is meant to prevent.
inner.entries.pop()(the loop at lines 951-955) already removes an entry — a real key-set change — beforeself.invalidate_keys_version()runs at line 957. Every other mutator in this file (insert,clear,delete_or_insert,setdefault,setdefault_entry,pop_inner_if) callsinvalidate_keys_version()before mutating, per the documented contract at lines 330-340: Reset the keys-version stamp on a key-set change ... Must be called while holding the write lock, before the key set is modified: a lock-free stamp reader that still observes the old stamp then provably ran before the change became visible, so acting on the old key set is linearizable.pop_backtakes&self(not&mut self), so a concurrent lock-free reader (e.g.shadowing_instance_attrin frame.rs, which readsdict.keys_version()without acquiring the RwLock) can observe the stale, not-yet-invalidated stamp in the window between thepop()and the invalidate call, incorrectly trusting a previously-cached "attribute verified absent" result even though the entry set already changed.🔒 Proposed fix: invalidate before popping
pub(crate) fn pop_back(&self) -> Option<(PyObjectRef, T)> { let inner = &mut *self.write(); + self.invalidate_keys_version(); let entry = loop { let entry = inner.entries.pop()?; if let Some(entry) = entry { break entry; } }; - self.invalidate_keys_version(); inner.used -= 1; *unsafe { // entry.index always refers valid index inner.indices.get_unchecked_mut(entry.index) } = IndexEntry::DUMMY; self.bump_version(); Some((entry.key, entry.value)) }This invalidates slightly eagerly (even if the dict turns out to be empty), which is always safe — invalidating too early never causes a false "unchanged" claim, only a possible wasted future re-probe.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/dict_inner.rs` around lines 949 - 965, Update pop_back to call invalidate_keys_version while holding the write lock before entering the loop that removes an entry from inner.entries. Preserve the existing empty-dictionary return behavior and all subsequent index, usage, and version updates.
🧹 Nitpick comments (3)
crates/vm/src/builtins/dict.rs (1)
796-811: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
keys_version()is missing theexact_dictguard thatassign_keys_version()enforces.
assign_keys_versiondeliberately never stamps dict subclasses because Returns 0 for dict subclasses: their lookup can be overridden, so a key-set attestation on the raw storage must never be cached for them. The readerkeys_version()has no equivalent check and directly returnsself.entries.keys_version()regardless ofexact_dict. Today this is masked because the write side never installs a stamp for non-exact dicts, but it's an asymmetric invariant: if the samePyDictobject's identity check ever flips (e.g.__class__reassignment onto a compatible dict subclass after it was already stamped as an exact dict),keys_version()could return a stale, previously-installed nonzero stamp that a caller wrongly trusts as attesting the subclass's current (possibly overridden) lookup semantics.🛡️ Proposed fix for consistency
- /// Current keys-version stamp of the underlying storage (0 if unset). - pub(crate) fn keys_version(&self) -> u32 { - self.entries.keys_version() - } + /// Current keys-version stamp of the underlying storage (0 if unset). + /// + /// Returns 0 for dict subclasses, matching `assign_keys_version`. + pub(crate) fn keys_version(&self, vm: &VirtualMachine) -> u32 { + if self.exact_dict(vm) { + self.entries.keys_version() + } else { + 0 + } + }This requires updating the call site in
frame.rs'sshadowing_instance_attrfromdict.keys_version()todict.keys_version(vm).Can you confirm whether
__class__reassignment betweendictand a layout-compatibledictsubclass is possible on an existing instance in RustPython? That determines whether this gap is reachable in practice.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/builtins/dict.rs` around lines 796 - 811, Update PyDict::keys_version to accept a VirtualMachine reference and return the underlying entries stamp only for exact_dict(vm), otherwise return 0, matching assign_keys_version. Update the shadowing_instance_attr call site in frame.rs to pass vm, preserving the invariant that dict subclasses never expose cached key-set attestations.extra_tests/snippets/vm_specialization.py (1)
130-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSilence the ruff B018 warning on the intentional attribute-access probe.
Line 131's bare
obj.xis intentional (it's meant to raiseAttributeError), but ruff flags it as a useless expression. Assign to_to keep the intent explicit while satisfying the linter.🔧 Proposed fix
try: - obj.x + _ = obj.x except AttributeError:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@extra_tests/snippets/vm_specialization.py` around lines 130 - 135, Update the intentional attribute-access probe in the try block to assign obj.x to _ before the AttributeError check, preserving the expected exception behavior while silencing ruff B018.Source: Linters/SAST tools
crates/vm/src/frame.rs (1)
4433-4437: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winInconsistent error handling between the two
shadowing_instance_attrcall sites.
LoadAttrMethodWithValuesswallows anErrfromshadowing_instance_attrand falls back toload_attr_slow, butLoadAttrNondescriptorWithValuespropagates the sameErrdirectly via?. Both are safe in the common case (the slow path is authoritative), but if the probe error stems from a side-effecting custom__eq__on a colliding dict key, the "swallow and retry" path could re-invoke that__eq__and potentially mask/duplicate the exception's side effects. Worth making both sites consistent (either always propagate, or always fall back with the same justification).Also applies to: 4568-4568
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/frame.rs` around lines 4433 - 4437, Make error handling consistent at both shadowing_instance_attr call sites in LoadAttrMethodWithValues and LoadAttrNondescriptorWithValues. Choose one behavior for shadowing_instance_attr errors—propagating the error or falling back to load_attr_slow—and apply it identically in both paths, preserving the intended handling of side-effecting equality failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/vm/src/dict_inner.rs`:
- Around line 949-965: Update pop_back to call invalidate_keys_version while
holding the write lock before entering the loop that removes an entry from
inner.entries. Preserve the existing empty-dictionary return behavior and all
subsequent index, usage, and version updates.
---
Nitpick comments:
In `@crates/vm/src/builtins/dict.rs`:
- Around line 796-811: Update PyDict::keys_version to accept a VirtualMachine
reference and return the underlying entries stamp only for exact_dict(vm),
otherwise return 0, matching assign_keys_version. Update the
shadowing_instance_attr call site in frame.rs to pass vm, preserving the
invariant that dict subclasses never expose cached key-set attestations.
In `@crates/vm/src/frame.rs`:
- Around line 4433-4437: Make error handling consistent at both
shadowing_instance_attr call sites in LoadAttrMethodWithValues and
LoadAttrNondescriptorWithValues. Choose one behavior for shadowing_instance_attr
errors—propagating the error or falling back to load_attr_slow—and apply it
identically in both paths, preserving the intended handling of side-effecting
equality failures.
In `@extra_tests/snippets/vm_specialization.py`:
- Around line 130-135: Update the intentional attribute-access probe in the try
block to assign obj.x to _ before the AttributeError check, preserving the
expected exception behavior while silencing ruff B018.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 03de58d1-52ef-468d-8df6-e9ae78daaab7
📒 Files selected for processing (5)
crates/compiler-core/src/bytecode.rscrates/vm/src/builtins/dict.rscrates/vm/src/dict_inner.rscrates/vm/src/frame.rsextra_tests/snippets/vm_specialization.py
Derive the keys-version stamp from a shape — the ordered interned-key sequence of a hole-free dict — instead of always allocating a unique stamp. Dicts with identical layouts (instances of the same class built by the same __init__) now carry equal stamps, so a LOAD_ATTR cache entry populated by one instance skips the shadow probe for every instance sharing the layout. Shapes are held in a fixed-size lock-free table keyed by interned key addresses; dicts with holes, non-interned keys, or more than 32 keys fall back to dict-unique stamps. Assisted-by: Claude
|
Added a second commit that shares keys-version stamps between dicts with identical layouts (CPython's shared-keys effect, without the memory-layout change): the stamp is now derived from a shape — the ordered interned-key sequence of a hole-free dict — held in a fixed-size lock-free table. Instances of the same class built by the same Polymorphic benchmark (100 instances round-robin through one call site, Apple M5 Max): method-load+call loop 126ns → 122ns/iter, method+attr loop 176ns → 161ns/iter. Monomorphic numbers are unchanged. Fallbacks (holes, non-interned keys, >32 keys, table pressure) keep dict-unique stamps, preserving the previous behavior. — commented by Claude |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@extra_tests/snippets/vm_specialization.py`:
- Line 178: Replace the direct lambda binding at
extra_tests/snippets/vm_specialization.py lines 178-178 by defining a nested
named function that returns "thirteen" and assigning it to objs[13].m; likewise,
at lines 204-204 define a nested named function returning "g2" and assign it to
g2.m. Preserve the existing behavior while removing both direct lambda
assignments.
- Around line 201-206: Update the specialization test around g1.m and g2.m to
add a shared helper such as call_m(obj) that performs the attribute access, then
invoke it for both objects during warmup and after g2.m is shadowed. Ensure both
holey dictionaries exercise the same LOAD_ATTR cache site while preserving the
expected "g" and "g2" results.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: c3ce656c-2299-462e-bfbb-66ed9de66154
📒 Files selected for processing (2)
crates/vm/src/dict_inner.rsextra_tests/snippets/vm_specialization.py
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/vm/src/dict_inner.rs
| for _ in range(300): | ||
| for o in objs: | ||
| assert o.m() == "method" | ||
| objs[13].m = lambda: "thirteen" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace direct lambda bindings with named functions. Both sites violate PEP 8’s guidance against assigning lambdas directly.
extra_tests/snippets/vm_specialization.py#L178-L178: define a nested function returning"thirteen"and assign it toobjs[13].m.extra_tests/snippets/vm_specialization.py#L204-L204: define a nested function returning"g2"and assign it tog2.m.
As per coding guidelines, **/*.py must follow PEP 8 style and use Ruff for linting.
📍 Affects 1 file
extra_tests/snippets/vm_specialization.py#L178-L178(this comment)extra_tests/snippets/vm_specialization.py#L204-L204
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@extra_tests/snippets/vm_specialization.py` at line 178, Replace the direct
lambda binding at extra_tests/snippets/vm_specialization.py lines 178-178 by
defining a nested named function that returns "thirteen" and assigning it to
objs[13].m; likewise, at lines 204-204 define a nested named function returning
"g2" and assign it to g2.m. Preserve the existing behavior while removing both
direct lambda assignments.
Source: Coding guidelines
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
crates/vm/src/builtins/dict.rs:779
- The doc comment for
set_item_with_hintsays the refreshed hint is returned only when the hinted slot was vacant, but the implementation (insert_with_hint) refreshes the hint on any hint miss (wrong key, out-of-bounds, or vacant) after falling back to a full insert/probe. This makes the current wording misleading for callers and future maintainers.
/// Store using a cached entry index hint for the value-replace fast path.
///
/// Returns a refreshed hint for the key when the hinted slot was vacant
/// (`None` otherwise).
- Use BuildHasher::hash_one for shape hashing (clippy manual_hash_one) - Fix set_item_with_hint doc: a refreshed hint is returned on any hint miss, not only when the hinted slot was vacant - Route both holey-dict instances through a single LOAD_ATTR cache site in the snippet test Assisted-by: Claude
| #[inline] | ||
| /// Store an instance attribute through the cached entry index at | ||
| /// `cache_base + 3`, refreshing the cache when the hint missed. | ||
| fn store_attr_dict_hinted( |
There was a problem hiding this comment.
Good catch — the doc block and #[inline] belonged to try_read_cached_descriptor and got orphaned when store_attr_dict_hinted was inserted above it. Restored in 88dd489.
— commented by Claude
The doc block and #[inline] were left attached to store_attr_dict_hinted when it was inserted above try_read_cached_descriptor. Assisted-by: Claude
| let instance_attr_hint = if let Some(dict) = obj.dict() { | ||
| match dict.hint_for_key(attr_name, _vm) { | ||
| Ok(hint) => hint, | ||
| Err(_) => { | ||
| unsafe { |
There was a problem hiding this comment.
Valid — hint_for_key conflated "absent" with "present but index >= 65536", so instances with huge dicts lost specialization entirely. Fixed in 6ccc0fe: the specializer now decides presence via get_item_opt_refresh_hint and degrades an unrepresentable hint to 0, so the opcode still specializes and just keeps taking the handler's full-probe fallback. (Note the same degradation was already the store-side behavior: specialize_store_attr maps a present-but-unrepresentable hint to StoreAttrInstanceValue, which stores through set_item_with_hint with hint 0.) Added a 70k-attribute regression test locally to confirm both load and store stay correct.
— commented by Claude
hint_for_key returns None both for an absent key and for a present key whose entry index does not fit in u16, so very large instance dicts stopped specializing entirely. Use get_item_opt_refresh_hint to decide presence, degrading an unrepresentable hint to 0: the handler then keeps taking its full-probe fallback path. Assisted-by: Claude
| // The key was absent at specialization time, but this | ||
| // very store inserts it; hint learning makes later | ||
| // executions replace by entry index. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/vm/src/frame.rs (1)
4732-4735: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winCorrect the store-path comment.
Lines [4732-4735] also handle existing dictionary entries whose hints are unrepresentable, not only absent keys. Update the comment to describe both cases.
Proposed comment fix
- // The key was absent at specialization time, but this - // very store inserts it; hint learning makes later - // executions replace by entry index. + // The key was absent or its hint was unrepresentable at + // specialization time; refresh the hint after this store + // so later executions can use entry-index replacement + // when possible.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/vm/src/frame.rs` around lines 4732 - 4735, Update the comment immediately above store_attr_dict_hinted in the relevant frame store path to describe both absent keys and existing dictionary entries whose hints are unrepresentable, while retaining the explanation that hint learning lets later executions replace by entry index.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@crates/vm/src/frame.rs`:
- Around line 4732-4735: Update the comment immediately above
store_attr_dict_hinted in the relevant frame store path to describe both absent
keys and existing dictionary entries whose hints are unrepresentable, while
retaining the explanation that hint learning lets later executions replace by
entry index.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro Plus
Run ID: 080f4d19-02ac-4950-904b-fc9ec8b859ed
📒 Files selected for processing (1)
crates/vm/src/frame.rs
Even after LOAD_ATTR specializes, the specialized handlers still paid for a full hash+probe dict lookup on every execution —
LoadAttrWithHintignored its hint,LoadAttrMethodWithValuesprobed the instance dict as its shadow check, andStoreAttr*re-probed on every store. This PR replaces those probes with two cheap guards.Changes
Dict keys-version stamp (
dict_inner.rs)Dictgets akeys_version: AtomicU32. Stamps are allocated from a global counter and never reused, so equal nonzero stamps guarantee an unchanged key set. Any key-set change (new key, deletion, pop, clear,drain_entries) resets the stamp while holding the write lock, before the mutation; value-only updates keep it.get_with_hint(lookup returning the entry index) andinsert_with_hint(identity-checked value replace by entry index, falling back to a full insert).LOAD_ATTR_METHOD_WITH_VALUES / LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES — the shadow check caches the instance dict's stamp in the instruction's pointer-cache slot. While the stamp matches, the per-call dict probe is skipped entirely (an integer compare, like CPython's keys-version check). The stamp is taken before the absence probe so a concurrent key-set change can never leave a stale skip. Dict subclasses never get a stamp and always probe.
LOAD_ATTR_WITH_HINT — the specializer now stores the entry index in the cache. A hit is a single identity check on the hinted entry's key — no hashing, no probe sequence. On miss the handler re-probes and refreshes the hint in place.
STORE_ATTR_WITH_HINT / STORE_ATTR_INSTANCE_VALUE — same treatment via
set_item_with_hint: value replace through the cached entry index, self-refreshing on miss.Results (Apple M5 Max, release, vs CPython 3.14.5)
c.xread overhead vs local readself.valuer/w +c.add(i)loopProfiles confirm the
Dict::gethash-probe frames (~7–10% of samples) are gone from both loops; the remaining gap is dominated by call overhead, int boxing, and dispatch (tracked separately).Testing
cargo testworkspace (the pre-existingrustpython-capipyo3 build failure is unrelated)-m testfor test_dict, test_descr, test_class, test_weakref, test_gc, test_property, test_super, test_builtin, test_types, test_copy, test_pickle, test_ordered_dict, test_userdict, test_collections — all SUCCESSextra_tests/snippets/vm_specialization.py(shadowing after warmup, value-only updates, hint invalidation across key churn /__dict__replacement /clear()), also validated against CPython 3.14Fixes youknowone#42
🤖 Generated with Claude Code
Summary by CodeRabbit
Performance
Bug Fixes
Tests