diff --git a/changelog.d/11658-read-miss-front-biased-receiver.md b/changelog.d/11658-read-miss-front-biased-receiver.md new file mode 100644 index 0000000000..8d3a864190 --- /dev/null +++ b/changelog.d/11658-read-miss-front-biased-receiver.md @@ -0,0 +1,10 @@ +A generic property read site passes its receiver to the GC-leaf miss front +(`js_object_get_field_ic_front`) as the value its fused receiver test already +holds (the payload minus the native-handle floor, now one shared constant in +perry-abi), so the site pays a register move where it paid a 10-byte constant +and an add; the front adds the floor back inside its load displacements. +Every polymorphic way hit and latched megamorphic read saves three +instructions (lead_poly4 132.0 -> 129.7 instr/iter, lead_mega1 164.3 -> +161.5). The way cascade now asserts, in debug builds, that an +overflow-encoded slot never enters a way: the front answers a way with a +plain inline load and no spill re-test. diff --git a/crates/perry-abi/src/lib.rs b/crates/perry-abi/src/lib.rs index 3ede028951..222d8c90b7 100644 --- a/crates/perry-abi/src/lib.rs +++ b/crates/perry-abi/src/lib.rs @@ -25,6 +25,13 @@ pub const AGENT_PTR_SLOTS: usize = 4; /// thread-local. Slot 1 held the implicit-`this` cell's address until /// this-as-a-parameter deleted the cell, and is free; slot 2 is the stack limit. pub const AGENT_PTR_SHAPE_DIR: usize = 0; +/// Payloads below this are native-registry handles, never heap cells +/// (`addr_class::HANDLE_BAND_MAX`). A generic read site's fused receiver test +/// computes `payload - RECEIVER_HANDLE_FLOOR` on its pointer edge, and its +/// miss front (`js_object_get_field_ic_front`) takes the receiver in exactly +/// that form: the front adds the floor back inside its load displacements, +/// and the site passes the value its test already holds. +pub const RECEIVER_HANDLE_FLOOR: usize = 0x10_0000; /// Slot 2: this agent's stack limit (#10812) — not a pointer to anything, the /// lowest frame address a compiled prologue accepts before it throws /// `RangeError: Maximum call stack size exceeded`. Null means unchecked. diff --git a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs index 3f8ec19c87..7b5b905158 100644 --- a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs +++ b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs @@ -1098,7 +1098,19 @@ pub(crate) fn lower_generic_property_get( EMPTY_SHAPE_DIR, ) }; - let front_handle = recv_handle(ctx, fused_recv.as_ref(), &entry_handle); + // The receiver as the fused test's biased value (payload minus + // `RECEIVER_HANDLE_FLOOR`, the front's operand form): one register + // move here, where the payload is a 10-byte constant and an add, + // since LLVM folds `biased + floor` back into `bits - POINTER_TAG`. + // A `length` site has no fused test and subtracts the floor itself. + let front_recv = match fused_recv.as_ref() { + Some(f) => f.biased.clone(), + None => ctx.block().sub( + I64, + &entry_handle, + &crate::runtime_abi::RECEIVER_HANDLE_FLOOR.to_string(), + ), + }; let key_box = ctx.block().load(DOUBLE, &key_handle_global); let key_bits = ctx.block().bitcast_double_to_i64(&key_box); let answered = ctx.block().call( @@ -1106,7 +1118,7 @@ pub(crate) fn lower_generic_property_get( "js_object_get_field_ic_front", &[ (PTR, &dir), - (I64, &front_handle), + (I64, &front_recv), (I64, &key_bits), (PTR, &cache_slot_ref), (PTR, &packed_ref), diff --git a/crates/perry-codegen/src/expr/receiver_range.rs b/crates/perry-codegen/src/expr/receiver_range.rs index 84a7b5bd13..9e58ab1ae7 100644 --- a/crates/perry-codegen/src/expr/receiver_range.rs +++ b/crates/perry-codegen/src/expr/receiver_range.rs @@ -34,7 +34,7 @@ use crate::types::{I32, I64, I8}; /// Payloads below this are native-registry handles, never heap cells /// (`js_native_call_method`'s small-handle test, `addr_class::HANDLE_BAND_MAX`). -pub(crate) const HANDLE_FLOOR: u64 = 0x10_0000; +pub(crate) const HANDLE_FLOOR: u64 = crate::runtime_abi::RECEIVER_HANDLE_FLOOR as u64; /// `POINTER_TAG | HANDLE_FLOOR`: subtracting it maps exactly the heap-object /// receivers onto `[0, RECEIVER_SPAN)`. pub(crate) const RECEIVER_BIAS: u64 = crate::nanbox::POINTER_TAG | HANDLE_FLOOR; 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 085598e68b..2fbbda0955 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 @@ -590,6 +590,13 @@ pub(crate) unsafe fn pic_prime_get(cache: *mut PicCache, token: i64, slot: i64) PIC_WAY_BASE + v as usize * 2 } }; + // First-read Q1: a way never holds a spill or overflow entry, so the miss + // front answers a way with a plain inline load and no spill re-test. + // `cascade` above is what guarantees it. + debug_assert!( + (prev_slot as u64) & u64::from(crate::proxy::IC_SLOT_OVERFLOW_BIT) == 0, + "an overflow-encoded slot must never enter a way" + ); c[ti] = prev_tok; c[ti + 1] = prev_slot; } diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss/read_confirm.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss/read_confirm.rs index 6ce271109e..94c73b07f6 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss/read_confirm.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/read_confirm.rs @@ -56,9 +56,14 @@ unsafe fn inline_slot(obj: *const ObjectHeader, slot: usize) -> f64 { /// nothing; never null. A `length` site passes the empty one on purpose: an /// Array-subclass receiver serves `length` from its elements store, which no /// key list names. -/// * `obj_handle` — the receiver's payload. The site calls only on the -/// ShapeId compare's false edge, which its small-handle test dominates, so -/// this is a real object pointer (its `+4` word was just loaded). +/// * `obj_biased` — the receiver's payload minus +/// `perry_abi::RECEIVER_HANDLE_FLOOR`: the value the site's fused receiver +/// test already holds on its pointer edge, so passing it costs the site a +/// register move where the payload cost a 10-byte constant and an add +/// (first-read D4: every way hit pays this edge). The front adds the floor +/// back in its load displacements. The site calls only on the ShapeId +/// compare's false edge, which its small-handle test dominates, so the +/// payload is a real object pointer (its `+4` word was just loaded). /// * `key_bits` — the site's key exactly as its pool global holds it: the /// interned key, STRING-tagged, which is also how a canonical key list /// stores it, so the confirm compares one word. @@ -74,12 +79,13 @@ unsafe fn inline_slot(obj: *const ObjectHeader, slot: usize) -> f64 { #[no_mangle] pub unsafe extern "C" fn js_object_get_field_ic_front( dir: *const u8, - obj_handle: i64, + obj_biased: i64, key_bits: u64, cache_slot: *mut PicCacheSlot, packed: *const AtomicU64, ) -> f64 { - let obj = obj_handle as usize as *const ObjectHeader; + let obj = + (obj_biased as usize).wrapping_add(perry_abi::RECEIVER_HANDLE_FLOOR) as *const ObjectHeader; let shape_id = (*obj).parent_class_id; // `pic_slot_peek` without its null test: the slot is the site's global. let cache = (*(cache_slot as *const std::sync::atomic::AtomicPtr)) @@ -93,8 +99,12 @@ pub unsafe extern "C" fn js_object_get_field_ic_front( (*cache)[PIC_WAY_STATE] }; if state > 0 { - // 1. The ways. A way token is `PIC_ID_TOKEN_BIT | ShapeId`; an empty - // way is 0 and cannot match. + // 1. The ways (first-read D4), in order, each hit loading its own + // way's slot; the ShapeId is the one loaded above. A way token is + // `PIC_ID_TOKEN_BIT | ShapeId`; an empty way is 0 and cannot match. + // No spill re-test: a way never holds a spill or overflow entry + // (`pic_prime_get` publishes a spill entry only to the compact word, + // and refuses to cascade an overflow-encoded slot into a way). let token = (shape_id as u64 | PIC_ID_TOKEN_BIT) as i64; for w in 0..PIC_WAYS { if (*cache)[PIC_WAY_BASE + 2 * w] == token { @@ -204,7 +214,8 @@ pub(crate) unsafe fn test_site_miss_read( crate::object::shapes::ordinary_dir_addr() }; let key_bits = key as usize as u64 | crate::value::STRING_TAG; - let v = js_object_get_field_ic_front(dir, obj_handle, key_bits, cache_slot, packed); + let obj_biased = obj_handle.wrapping_sub(perry_abi::RECEIVER_HANDLE_FLOOR as i64); + let v = js_object_get_field_ic_front(dir, obj_biased, key_bits, cache_slot, packed); if v.to_bits() != crate::value::TAG_HOLE { return v; } @@ -347,7 +358,9 @@ mod tests { let packed = AtomicU64::new(word); let dir = crate::object::shapes::ordinary_dir_addr(); let bits = unsafe { - super::js_object_get_field_ic_front(dir, obj as i64, key_bits, cache_slot, &packed) + // The operand form a site passes (`obj_biased`, see the front). + let biased = (obj as i64).wrapping_sub(perry_abi::RECEIVER_HANDLE_FLOOR as i64); + super::js_object_get_field_ic_front(dir, biased, key_bits, cache_slot, &packed) .to_bits() }; (bits, packed.load(Ordering::Relaxed)) diff --git a/crates/perry-runtime/src/value/addr_class.rs b/crates/perry-runtime/src/value/addr_class.rs index b30c80e529..f519aeba04 100644 --- a/crates/perry-runtime/src/value/addr_class.rs +++ b/crates/perry-runtime/src/value/addr_class.rs @@ -45,6 +45,8 @@ use crate::gc::{GcHeader, GC_HEADER_SIZE}; /// Raising any sub-band past this value requires auditing every /// `is_handle_band` caller. pub const HANDLE_BAND_MAX: usize = 0x100000; +// Emitted receiver tests and the read miss front share this floor. +const _: () = assert!(HANDLE_BAND_MAX == perry_abi::RECEIVER_HANDLE_FLOOR); /// Exclusive end of the generic perry-stdlib `common/handle.rs` registry band /// (`[1, COMMON_HANDLE_BAND_END)`). The registry panics rather than allocate