Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions changelog.d/inherited-read-holder-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
An inherited or absent property read is now answered by the read site itself,
from facts of two shapes, instead of the inherited-read side table. When `o.k`
misses because `k` is not own, the miss handler records in the site cache the
receiver ShapeId, the object that holds `k` (a strong GC root), that
object ShapeId and the slot; an absent key records the terminal object
instead. The emitted read compares the receiver ShapeId and the holder
ShapeId and loads the slot inline (depth 1), or calls a GC-leaf stub that also
compares the intermediate hops (depth 2 to 4). A key added, deleted or
redefined on any object on the chain, or a `setPrototypeOf`, moves a ShapeId
the entry compares; a value store is seen because the slot is loaded. No global
validity word is involved. `process.env` and `arguments` now carry a per-object
prototype identity in their shape, so no shape-keyed memo admits them.

The entry is asked where the site own-slot word and its polymorphic ways
miss (a megamorphic site goes straight to the call, as before). A receiver it
does not describe (a compiled-class instance) keeps the inherited-read hook
it had before on the never-primed edge, and a site that refused once, or was
re-primed for four different receiver shapes, is latched: it stops priming,
so its misses do not walk the chain and run the getter again, and it asks the
inherited-read hook as a never-primed site does. A miss the live entry
answers primes nothing (the class-field read miss arm asks the entry
directly).
18 changes: 18 additions & 0 deletions crates/perry-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,24 @@ pub const METHOD_SITE_CODE_OFFSET: usize = 40;
/// Entries per method site, and one entry's size.
pub const METHOD_SITE_WAYS: usize = 2;
pub const METHOD_SITE_ENTRY_SIZE: usize = 48;

/// `object::method_site::read_holder` — the property-read cache words
/// (`PicCache`) holding the read site's holder entry, which the emitted read
/// tower checks where the MRU word and the ways miss
/// (`perry-codegen/src/expr/property_get/generic_dispatch.rs`).
pub const PIC_HOLDER_RECV_WORD: usize = 12;
pub const PIC_HOLDER_OBJ_WORD: usize = 13;
pub const PIC_HOLDER_SHAPE_WORD: usize = 14;
pub const PIC_HOLDER_KIND_WORD: usize = 15;
/// The site's holder state word, and its bit for a LATCHED site: one that
/// refused, or whose non-own receivers took several shapes. Its misses ask the
/// inherited-read hook, as a never-primed site's do.
pub const PIC_HOLDER_STATE_WORD: usize = 20;
pub const PIC_HOLDER_STATE_LATCHED: i64 = 2;
/// The kind word of a depth-1 ABSENT entry: the answer is `undefined`.
pub const PIC_HOLDER_ABSENT_DEPTH1: i64 = 1 << 62;
/// Words in a property-read cache: MRU, way state, four ways, the holder entry.
pub const PIC_CACHE_WORDS: usize = 21;
/// A method site calls a body with its argument count padded by `undefined`
/// up to this many extra arguments (never past 16), and admits bodies that
/// declare up to that many parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::types::{DOUBLE, I1, I32, I64, I8, PTR};
/// here and `pic_cache_words_match_codegen` in the runtime: change one and both
/// fail.
#[cfg(test)]
pub(crate) const PIC_CACHE_WORDS: usize = 12;
pub(crate) const PIC_CACHE_WORDS: usize = 21;
/// First word of the polymorphic way array (words 0..2 are the MRU entry and
/// word 3 is the gate). Mirrors the runtime's `PIC_WAY_BASE`.
#[cfg(test)]
Expand Down
10 changes: 6 additions & 4 deletions crates/perry-codegen/src/expr/property_get/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,16 @@ fn pic_cache_layout_matches_runtime() {
"the way-state word sits below the ways, as in perry-runtime"
);
assert_eq!(
PIC_CACHE_WORDS, 12,
"perry-runtime's PIC_CACHE_WORDS is 12; update both sides together"
PIC_CACHE_WORDS, 21,
"perry-runtime's PIC_CACHE_WORDS is 21; update both sides together"
);
assert_eq!(
PIC_WAY_BASE + PIC_WAYS * 2,
PIC_CACHE_WORDS,
"the ways must fill the emitted global exactly"
crate::runtime_abi::PIC_HOLDER_RECV_WORD,
"the holder entry starts where the ways end"
);
assert!(crate::runtime_abi::PIC_HOLDER_KIND_WORD < PIC_CACHE_WORDS);
assert_eq!(crate::runtime_abi::PIC_CACHE_WORDS, PIC_CACHE_WORDS);
let ir = emit(false, None);
let ic_defs: Vec<&str> = ir
.lines()
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,10 @@ pub fn gc_init() {
// Method-calls lane: an inherited method-site entry holds the method
// closure it calls, so the closure is a STRONG root (`object::method_site`).
reg_scanner!(crate::object::method_site::scan_method_site_roots_mut);
// A read site's holder entry names the object that holds the answer (and
// the hops to it); the emitted hit loads through it, so each is a STRONG
// root (`object::method_site::read_holder`).
reg_scanner!(crate::object::method_site::read_holder::scan_read_holder_roots_mut);
reg_scanner!(crate::map::scan_map_iterator_array_roots_mut);
reg_scanner!(crate::set::scan_set_iterator_array_roots_mut);
reg_scanner!(crate::perf_hooks::scan_perf_entries_roots_mut);
Expand Down
19 changes: 19 additions & 0 deletions crates/perry-runtime/src/gc/tests/arguments_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,22 @@ fn only_the_state_word_identifies_an_arguments_object() {
assert_eq!(test_arguments_mapped_box(restricted, 0), None);
assert!(test_arguments_mapping_array(restricted).is_none());
}

/// The exotic flag is a SHAPE fact: marking moves the receiver to a shape
/// whose [[Prototype]] identity is its own, which no shape-keyed read memo
/// admits (`object::method_site::read_holder`), and a key added afterwards
/// keeps it.
#[test]
fn the_exotic_flag_moves_the_receiver_to_a_per_object_identity() {
let _guard = GcTestIsolationGuard::with_realm_bootstrapped();
let obj = js_object_alloc(0, 1);
let before = unsafe { crate::object::shapes::object_shape_stamp(obj) };
unsafe { crate::object::proto_validity::mark_exotic_read_receiver(obj as usize) };
let after = unsafe { crate::object::shapes::object_shape_stamp(obj) };
assert_ne!(before, after, "marking must move the receiver's ShapeId");
assert_eq!(
crate::object::shapes::shape_proto_id(after),
Some(crate::object::shapes::PROTO_ID_PER_OBJECT),
"the marked receiver's shape must carry a per-object identity"
);
}
26 changes: 21 additions & 5 deletions crates/perry-runtime/src/object/field_get_set/ic_miss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub(crate) fn set_method_value_name(key: &[u8]) -> Option<&'static [u8]> {
/// Words in a per-site property-read cache global (`@perry_ic_N`). Codegen
/// emits `[PIC_CACHE_WORDS x i64] zeroinitializer`; this type is the runtime's
/// view of the same memory.
pub const PIC_CACHE_WORDS: usize = 12;
pub const PIC_CACHE_WORDS: usize = crate::codegen_abi::PIC_CACHE_WORDS;

/// The runtime view of a `@perry_ic_N` property-read cache.
///
Expand All @@ -222,7 +222,8 @@ pub const PIC_CACHE_WORDS: usize = 12;
/// | 1 | `slot0` — its resolved field slot |
/// | 2 | unused — was the Array-subclass named-prefix token, retired by S6 (site state must derive from one shape) |
/// | 3,4 / 5,6 / 7,8 / 9,10 | `(tok, slot)` ways |
/// | 11 | round-robin victim index for the ways |
/// | 3 | way state ([`PIC_WAY_STATE`]) |
/// | 12..=20 | the holder entry for a key that is not own (`method_site::read_holder`) |
pub type PicCache = [i64; PIC_CACHE_WORDS];

/// The value a per-site compact MRU word (`@perry_ic_N_packed_get`) holds
Expand Down Expand Up @@ -1135,6 +1136,13 @@ pub(super) fn get_field_ic_miss_impl(
// +106 instructions per read against the same binary with
// `PERRY_INHERITED_IC=0`, i.e. the cache was pure overhead for
// this shape.
// The site's holder entry: primed here, answered by the
// emitted tower from then on (`method_site::read_holder`).
if let Some(value) =
crate::object::method_site::read_holder::prime_read_holder(obj, key, cache_slot)
{
return f64::from_bits(value.bits());
}
if !inherited_declined {
// Already inside this function's `unsafe` block (line 874),
// so a nested one is `unused_unsafe` under -D warnings.
Expand Down Expand Up @@ -1288,6 +1296,14 @@ pub(super) fn get_field_ic_miss_impl(
// paying for a second search. Walk the chain once and record the answer.
// A decline leaves the generic getter below untouched, which is today's
// behaviour for every case the cache refuses.
if matches!(miss_reason, R::NotOwn) {
// The site's holder entry (`method_site::read_holder`).
if let Some(value) = unsafe {
crate::object::method_site::read_holder::prime_read_holder(obj, key, cache_slot)
} {
return f64::from_bits(value.bits());
}
}
if matches!(miss_reason, R::NotOwn) && !inherited_declined {
if let Some(value) =
unsafe { crate::object::inherited_read_cache::inherited_read_cache_prime(obj, key) }
Expand Down Expand Up @@ -2431,8 +2447,8 @@ mod poly_pic_tests {
#[test]
fn pic_cache_words_match_codegen() {
assert_eq!(
PIC_CACHE_WORDS, 12,
"codegen emits `[12 x i64]`; update both sides together"
PIC_CACHE_WORDS, 21,
"codegen's PIC_CACHE_WORDS is 21; update both sides together"
);
assert!(
PIC_WAY_STATE < PIC_CACHE_WORDS,
Expand All @@ -2444,7 +2460,7 @@ mod poly_pic_tests {
);
assert_eq!(
PIC_WAY_BASE + PIC_WAYS * 2,
PIC_CACHE_WORDS,
crate::codegen_abi::PIC_HOLDER_RECV_WORD,
"the ways must fill the global exactly"
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
//! compact word's high half), confirmed by the receiver's own shape — the
//! shape record's `POSBOUND` and its canonical key list compared with the
//! key atom — and, on a wrong guess, one bounded scan of that key list that
//! re-aims the guess (D3b).
//! re-aims the guess (D3b);
//! 4. the site's HOLDER entry (`method_site::read_holder`): a key that is not
//! own, answered from the receiver's shape and the holder's.
//!
//! Anything else answers `TAG_HOLE`, and only then does the site branch to
//! its cold block and call the collecting `js_object_get_field_ic_slow` with
Expand Down Expand Up @@ -131,7 +133,22 @@ pub unsafe extern "C" fn js_object_get_field_ic_front(
}
if state < 0 {
// 3. Latched.
return confirm_in(dir, obj, shape_id, packed, word, key_bits);
let own = confirm_in(dir, obj, shape_id, packed, word, key_bits);
if own.to_bits() != crate::value::TAG_HOLE || cache.is_null() {
return own;
}
}
// 4. The site's HOLDER entry (`method_site::read_holder`): the answer for a
// key that is NOT own on this receiver, as facts of the receiver's shape
// and the holder's. Asked last, so an own-key read pays nothing for it.
// A GC leaf like everything above: it reads site words and object words.
if !cache.is_null() {
if let Some(bits) = crate::object::method_site::read_holder::entry_answer(
&*cache,
(shape_id as u64 | PIC_ID_TOKEN_BIT) as i64,
) {
return f64::from_bits(bits);
}
}
hole()
}
Expand Down
6 changes: 5 additions & 1 deletion crates/perry-runtime/src/object/method_site.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
//! a worker never reads a primary-heap closure through a site.

use crate::object::ObjectHeader;

pub(crate) mod read_holder;
use std::sync::atomic::{AtomicU64, Ordering};

/// `word` of a site no prime has touched: no receiver word is all-ones.
Expand Down Expand Up @@ -201,6 +203,7 @@ static WORKER_AGENTS_EXIST: std::sync::atomic::AtomicBool =
pub fn note_worker_agent() {
if !WORKER_AGENTS_EXIST.swap(true, Ordering::SeqCst) {
super::proto_validity::bump_proto_validity();
read_holder::empty_read_holder_entries();
}
}

Expand Down Expand Up @@ -285,8 +288,9 @@ fn stats_report_enabled() -> bool {
refused.push_str(&format!(" refused.{}={n}", REFUSALS[i]));
}
}
let (hd, ha, hr) = read_holder::read_holder_stats();
eprintln!(
"[method-site] primes_own={a} primes_inherited={b} primes_function={} misses={c} marked_value_write_bumps={}{refused}",
"[method-site] primes_own={a} primes_inherited={b} primes_function={} misses={c} read_holder_primes={hd} read_absent_primes={ha} read_holder_refused={hr} marked_value_write_bumps={}{refused}",
method_site_function_primes(),
crate::object::proto_validity::marked_value_write_bumps()
);
Expand Down
Loading
Loading