From e969427bb52e5f999710235f3e9790986422f117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 26 Aug 2026 06:51:12 +0200 Subject: [PATCH 1/2] perf: cache owning Uint32Array admissions --- crates/perry-runtime/src/typedarray/mod.rs | 71 +++++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index d2af56b85a..eeadc0a0f4 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -233,6 +233,18 @@ pub const TA_CACHE_NEGATIVE: u64 = 0xFF; pub static PERRY_TA_KIND_CACHE: [AtomicU64; TA_KIND_CACHE_SLOTS] = [const { AtomicU64::new(0) }; TA_KIND_CACHE_SLOTS]; +// The generic kind cache deliberately uses the exact slot formula duplicated +// by codegen. Large, equal-sized ECS columns can therefore share the same low +// address bits and continually evict one another. Whole-loop admission needs +// the stronger, persistent fact "this exact address is an owning Uint32Array", +// so keep a separate direct cache whose index folds higher address bits too. +// A hit is safe until unregister: a TypedArray header's kind and owning/view +// storage class never change during its lifetime, and unregister clears both +// caches before an address can be reused. +const INLINE_OWNING_U32_CACHE_SLOTS: usize = 64; +static INLINE_OWNING_U32_CACHE: [AtomicU64; INLINE_OWNING_U32_CACHE_SLOTS] = + [const { AtomicU64::new(0) }; INLINE_OWNING_U32_CACHE_SLOTS]; + /// #5525 follow-up: process-global "any exotic typed-array views exist" guard, /// exported under a stable link name for the codegen inline element path. A /// non-owning typed array (an `ArrayBuffer`-aliasing view, or a native-arena @@ -287,6 +299,33 @@ fn ta_kind_cache_invalidate(addr: usize) { } } +#[inline] +fn inline_owning_u32_cache_slot(addr: usize) -> usize { + let word = addr >> 3; + let mixed = word ^ (word >> 6) ^ (word >> 12); + mixed & (INLINE_OWNING_U32_CACHE_SLOTS - 1) +} + +#[inline] +fn inline_owning_u32_cache_get(addr: usize) -> bool { + INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)].load(Ordering::Relaxed) + == addr as u64 +} + +#[inline] +fn inline_owning_u32_cache_store(addr: usize) { + INLINE_OWNING_U32_CACHE[inline_owning_u32_cache_slot(addr)] + .store(addr as u64, Ordering::Relaxed); +} + +#[inline] +fn inline_owning_u32_cache_invalidate(addr: usize) { + let slot = inline_owning_u32_cache_slot(addr); + if INLINE_OWNING_U32_CACHE[slot].load(Ordering::Relaxed) == addr as u64 { + INLINE_OWNING_U32_CACHE[slot].store(0, Ordering::Relaxed); + } +} + /// Cache probe: `None` = miss (consult the registry), `Some(None)` = cached /// negative ("not a typed array"), `Some(Some(kind))` = cached typed array. #[inline] @@ -339,6 +378,7 @@ pub(crate) fn typed_array_registry_ever_used() -> bool { pub fn unregister_typed_array(ptr: *const TypedArrayHeader) { let owner = ptr as usize; ta_kind_cache_invalidate(owner); + inline_owning_u32_cache_invalidate(owner); TYPED_ARRAY_REGISTRY.with(|r| { r.borrow_mut().remove(&owner); }); @@ -587,9 +627,11 @@ pub extern "C" fn js_typed_array_masked_window_data_ptr(receiver: f64) -> i64 { /// One-time loop admission primitive for erased ECS component columns. Return /// the stable owning-header address only for an exact inline `Uint32Array`. -/// Consult the authoritative registry instead of the tiny direct-mapped kind -/// cache: sibling columns can collide there, which is harmless for individual -/// accesses but must not make a whole-loop proof spuriously fail forever. +/// Use the admission-specific address cache first, then consult the +/// authoritative registry on a miss. The generic direct-mapped kind cache is +/// intentionally not authority here: sibling columns can collide there, +/// which is harmless for individual accesses but must not make a whole-loop +/// proof spuriously fail forever. #[inline] pub(crate) fn inline_u32_addr(receiver: f64) -> usize { let value = crate::value::JSValue::from_bits(receiver.to_bits()); @@ -597,12 +639,16 @@ pub(crate) fn inline_u32_addr(receiver: f64) -> usize { return 0; } let addr = value.as_pointer::() as usize; + if inline_owning_u32_cache_get(addr) { + return addr; + } if lookup_typed_array_kind(addr) != Some(KIND_UINT32) || crate::native_arena::is_native_typed_view(addr as *const TypedArrayHeader) || crate::typedarray_view::view_meta_of(addr).is_some() { return 0; } + inline_owning_u32_cache_store(addr); addr } @@ -1253,6 +1299,25 @@ pub extern "C" fn js_native_memory_copy(dst_raw: u64, src_raw: u64) { mod tests { use super::*; + #[test] + fn owning_u32_admission_cache_skips_registry_and_invalidates() { + let ta = typed_array_alloc(KIND_UINT32, 16); + let boxed = crate::value::js_nanbox_pointer(ta as i64); + + let before = test_typed_array_registry_probe_count(); + assert_eq!(inline_u32_addr(boxed), ta as usize); + let primed = test_typed_array_registry_probe_count(); + assert_eq!(primed, before + 1); + assert_eq!(inline_u32_addr(boxed), ta as usize); + assert_eq!(test_typed_array_registry_probe_count(), primed); + + unregister_typed_array(ta); + assert_eq!(inline_u32_addr(boxed), 0); + assert_eq!(test_typed_array_registry_probe_count(), primed + 1); + // Leave the live allocation registered for its eventual finalizer. + register_typed_array(ta, KIND_UINT32); + } + #[test] fn large_object_typed_array_alloc_uses_old_gc_header_and_stays_usable() { let ta = typed_array_alloc(KIND_UINT8, crate::gc::LARGE_OBJECT_THRESHOLD_BYTES as u32); From 15d7673b294d03a53d8227c8935e3b561f8cad05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 26 Aug 2026 06:51:12 +0200 Subject: [PATCH 2/2] perf: fast-path Array subclass length misses --- .../src/object/field_get_set/ic_miss.rs | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index f6f1d9949d..a0aa5f574f 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -508,19 +508,35 @@ pub extern "C" fn js_object_get_field_ic_miss( // run time — more than the entire polymorphic-dispatch fix above saved. // // `GC_TYPE_ARRAY` is a genuine dense array: buffers, typed arrays, lazy - // arrays, Sets and Maps all carry their own distinct `obj_type`, and an - // `class X extends Array` instance is an `ObjectHeader` - // (`GC_TYPE_OBJECT`). `js_array_length` still resolves growth-forwarding - // stubs, proxies and subclass receivers, so this only skips probes that - // cannot match — the expression returned is exactly the one - // `get_field_by_name_object_tail`'s array arm computes for this key, - // which is what makes it a pure short-circuit rather than a second - // implementation. - if unsafe { gc_type_of(obj) } == Some(crate::gc::GC_TYPE_ARRAY) - && unsafe { key_bytes_are(key, b"length") } - { - let arr = obj as *const crate::array::ArrayHeader; - return crate::array::js_array_length(arr) as f64; + // arrays, Sets and Maps all carry their own distinct `obj_type`. A + // `class X extends Array` instance instead uses `GC_TYPE_OBJECT`, but + // the exact-ShapeId dense-layout proof can read its live own `length` + // slot without repeating generic object dispatch. Both arms retain + // their established helpers, making this a dispatch short-circuit + // rather than a second implementation of either representation. + if unsafe { key_bytes_are(key, b"length") } { + match unsafe { gc_type_of(obj) } { + Some(crate::gc::GC_TYPE_ARRAY) => { + let arr = obj as *const crate::array::ArrayHeader; + return crate::array::js_array_length(arr) as f64; + } + Some(crate::gc::GC_TYPE_OBJECT) => { + // Wolf ECS's Query and Archetype are `class ... extends + // Array` instances. They use ObjectHeader storage, so the + // Array arm above cannot recognize them and a megamorphic + // `.length` site otherwise repeats the full object lookup + // on every loop entry. Reuse the exact ShapeId-backed + // subclass layout proof already used by packed numeric + // reads. It declines accessor, prototype-override, sparse, + // and non-Array-subclass receivers, preserving the generic + // lookup below for every case it cannot prove. + let receiver = crate::value::js_nanbox_pointer(obj as i64); + if let Some(length) = crate::array::array_subclass_fast_length(receiver) { + return length; + } + } + _ => {} + } } unsafe { if let Some(val) = closure_dynamic_prop_by_key(obj as usize, key) { @@ -1955,4 +1971,40 @@ mod array_length_fast_path_tests { ); } } + + /// Array subclasses are ObjectHeader-backed, so a polymorphic loop over + /// differently shaped instances cannot use the real-Array short circuit + /// above or reliably stay in one property PIC. The dense subclass proof + /// must return the live own `length`, while unrelated object-backed values + /// continue through ordinary property lookup. + #[test] + fn array_subclass_length_short_circuit_preserves_object_semantics() { + const CLASS_ID_ARRAY: u32 = 0xFFFF_0024; + const SUBCLASS_ID: u32 = 0x0077_8655; + let _lock = crate::gc::global_side_table_test_lock(); + crate::object::js_register_class_parent(SUBCLASS_ID, CLASS_ID_ARRAY); + + let obj = crate::object::js_object_alloc(SUBCLASS_ID, 2); + let receiver = crate::value::js_nanbox_pointer(obj as i64); + crate::node_stream::js_array_subclass_init(receiver, 0.0); + for (index, value) in [11.0, 22.0, 33.0].into_iter().enumerate() { + crate::object::js_object_set_index_polymorphic(obj as i64, index as f64, value); + } + + let len_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); + let mut cache = [0i64; super::PIC_CACHE_WORDS]; + let via_ic = super::js_object_get_field_ic_miss(obj, len_key, &mut cache); + let via_ladder = super::js_object_get_field_by_name_f64(obj, len_key); + assert_eq!(via_ic.to_bits(), via_ladder.to_bits()); + assert_eq!(via_ic, 3.0, "the fast path must observe the live length"); + + let plain = crate::object::js_object_alloc(0, 1); + crate::object::js_object_set_field_by_name(plain, len_key, 123.0); + let mut plain_cache = [0i64; super::PIC_CACHE_WORDS]; + assert_eq!( + super::js_object_get_field_ic_miss(plain, len_key, &mut plain_cache), + 123.0, + "ordinary objects must retain their own `length` property semantics" + ); + } }