Skip to content

Commit 8d7d9d9

Browse files
authored
Use dict keys-version stamps and entry-index hints in LOAD_ATTR/STORE_ATTR specializations (#8350)
* Use dict keys-version stamps and entry-index hints in attr specializations 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 youknowone#42 Assisted-by: Claude * Share keys-version stamps between dicts with identical layouts 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 * Address clippy and review feedback for keys-version stamps - 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 * Restore try_read_cached_descriptor doc comment to its function 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 * Specialize LoadAttrWithHint even when the entry index exceeds u16 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
1 parent dd9bce5 commit 8d7d9d9

5 files changed

Lines changed: 550 additions & 40 deletions

File tree

crates/compiler-core/src/bytecode.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -776,12 +776,15 @@ impl CodeUnits {
776776
/// Store a pointer-sized value atomically in the pointer cache at `index`.
777777
///
778778
/// Uses a single `AtomicUsize` store to prevent torn writes when
779-
/// multiple threads specialize the same instruction concurrently.
779+
/// multiple threads specialize the same instruction concurrently. The
780+
/// tear-free width also makes this the right slot for non-pointer guard
781+
/// values (e.g. dict keys-version stamps) that must never be observed
782+
/// half-written.
780783
///
781784
/// # Safety
782785
/// - `index` must be in bounds.
783-
/// - `value` must be `0` or a valid `*const PyObject` encoded as `usize`.
784-
/// - Callers must follow the cache invalidation/upgrade protocol:
786+
/// - When the slot holds a `*const PyObject` encoded as `usize` (or `0`),
787+
/// callers must follow the cache invalidation/upgrade protocol:
785788
/// invalidate the version guard before writing and publish the new
786789
/// version after writing.
787790
pub unsafe fn write_cache_ptr(&self, index: usize, value: usize) {

crates/vm/src/builtins/dict.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,63 @@ impl Py<PyDict> {
753753
}
754754
}
755755

756+
/// Lookup trying a cached entry index hint first.
757+
///
758+
/// When the hint misses but the key is present, also returns a refreshed
759+
/// hint (`None` when the hint hit or no hint is representable).
760+
pub(crate) fn get_item_opt_refresh_hint<K: DictKey + ?Sized>(
761+
&self,
762+
key: &K,
763+
hint: u16,
764+
vm: &VirtualMachine,
765+
) -> PyResult<Option<(PyObjectRef, Option<u16>)>> {
766+
if self.exact_dict(vm) {
767+
if let Some(value) = self.entries.get_hint(vm, key, usize::from(hint))? {
768+
return Ok(Some((value, None)));
769+
}
770+
self.entries.get_with_hint(vm, key)
771+
} else {
772+
Ok(self.get_item_opt(key, vm)?.map(|value| (value, None)))
773+
}
774+
}
775+
776+
/// Store using a cached entry index hint for the value-replace fast path.
777+
///
778+
/// On a hint miss, returns a refreshed hint for the key (`None` when the
779+
/// hint hit or no hint is representable).
780+
pub(crate) fn set_item_with_hint<K: DictKey + ?Sized>(
781+
&self,
782+
key: &K,
783+
hint: u16,
784+
value: PyObjectRef,
785+
vm: &VirtualMachine,
786+
) -> PyResult<Option<u16>> {
787+
if self.exact_dict(vm) {
788+
self.entries
789+
.insert_with_hint(vm, key, usize::from(hint), value)
790+
} else {
791+
self.as_object().set_item(key, value, vm)?;
792+
Ok(None)
793+
}
794+
}
795+
796+
/// Current keys-version stamp of the underlying storage (0 if unset).
797+
pub(crate) fn keys_version(&self) -> u32 {
798+
self.entries.keys_version()
799+
}
800+
801+
/// Current keys-version stamp, assigning one if none is set.
802+
///
803+
/// Returns 0 for dict subclasses: their lookup can be overridden, so a
804+
/// key-set attestation on the raw storage must never be cached for them.
805+
pub(crate) fn assign_keys_version(&self, vm: &VirtualMachine) -> u32 {
806+
if self.exact_dict(vm) {
807+
self.entries.assign_keys_version()
808+
} else {
809+
0
810+
}
811+
}
812+
756813
pub fn get_item<K: DictKey + ?Sized>(&self, key: &K, vm: &VirtualMachine) -> PyResult {
757814
if self.exact_dict(vm) {
758815
self.inner_getitem(key, vm)

0 commit comments

Comments
 (0)