From 7853cda8284a8ad39d10489b448138712b970a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 11 Aug 2026 18:14:03 +0200 Subject: [PATCH 1/2] perf(runtime): let the GC header pick the side-registry probe on dynamic dispatch (#7850) Claude-Session: https://claude.ai/code/session_012B8z92S82sCfqCrVqrFgS2 --- .../src/object/native_call_method.rs | 55 +++- .../probe_dispatch_tests.rs | 278 ++++++++++++++++++ crates/perry-runtime/src/symbol.rs | 74 +++++ 3 files changed, 402 insertions(+), 5 deletions(-) create mode 100644 crates/perry-runtime/src/object/native_call_method/probe_dispatch_tests.rs diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 680ea03528..d3a8dfa5ab 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -19,6 +19,8 @@ mod string_methods; #[cfg(test)] mod dispatch_arg_coercion_tests; +#[cfg(test)] +mod probe_dispatch_tests; mod typed_array; use disposal::{ @@ -915,15 +917,58 @@ unsafe fn gc_pointer_and_type_from_value(value: f64) -> Option<(*const u8, u8)> if !is_valid_obj_ptr(ptr as *const u8) { return None; } - if crate::set::is_registered_set(addr) - || crate::map::is_registered_map(addr) - || crate::regex::is_regex_pointer(ptr as *const u8) - || crate::symbol::is_registered_symbol(addr) + // #7850. This used to run FOUR side-registry probes unconditionally before + // reading the `GcHeader` — and the header already records the kind that + // three of them are looking for. `is_registered_symbol` in particular takes + // a process-global `Mutex` plus a SipHash once ANY `Symbol` exists, which a + // single `for…of` (it materializes `Symbol.iterator`) makes true of almost + // every realistic program; it was 6.5% of `pipeline`'s samples, on the path + // of every dynamic method call. + // + // Read the header ONCE and let `obj_type` select the only probe that can + // possibly fire. Each implication below is enforced by the probe itself, so + // this is a re-ordering rather than a new assumption: + // + // * `set::is_registered_set` ends in `obj_type == GC_TYPE_SET`; + // * `map::is_registered_map` ends in `obj_type == GC_TYPE_MAP`; + // * `regex::is_regex_pointer` matches the header magic of a + // `gc_malloc(_, GC_TYPE_OBJECT)` allocation, and the sole + // `REGEX_POINTERS` insert (`js_regexp_new`) allocates exactly that; + // * a `Symbol` of any storage carries `SYMBOL_MAGIC` in its first word. + // + // The one kind the header cannot speak for is the `Box`-leaked symbol + // (`Symbol.for`, the well-knowns, the Intl fallback): it has no `GcHeader` + // at all, so `ptr - 8` is foreign allocator bytes that can coincidentally + // equal any `obj_type`. What every symbol DOES have, whatever its storage, + // is `SYMBOL_MAGIC` in its own first four bytes — so screen on the object's + // content, not on the header. `may_be_symbol_header` is exact in the + // `false` direction, and a false `true` merely pays the old probe. + if crate::symbol::may_be_symbol_header(ptr as *const u8) + && crate::symbol::is_registered_symbol(addr) { return None; } let gc_header = (ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; - Some((ptr, (*gc_header).obj_type)) + let obj_type = (*gc_header).obj_type; + let excluded = match obj_type { + crate::gc::GC_TYPE_SET => crate::set::is_registered_set(addr), + crate::gc::GC_TYPE_MAP => crate::map::is_registered_map(addr), + crate::gc::GC_TYPE_OBJECT => crate::regex::is_regex_pointer(ptr as *const u8), + _ => false, + }; + if excluded { + return None; + } + Some((ptr, obj_type)) +} + +/// Test hook for the header-directed probe dispatch above (#7850). Lets a unit +/// test assert BOTH halves of the claim: that a plain-object receiver no longer +/// moves the symbol/map/set probe counters, and that a Set/Map/RegExp/Symbol +/// receiver is still classified the same way it was before the re-ordering. +#[cfg(test)] +pub(crate) unsafe fn test_gc_pointer_and_type_from_value(value: f64) -> Option<(*const u8, u8)> { + gc_pointer_and_type_from_value(value) } #[inline] diff --git a/crates/perry-runtime/src/object/native_call_method/probe_dispatch_tests.rs b/crates/perry-runtime/src/object/native_call_method/probe_dispatch_tests.rs new file mode 100644 index 0000000000..876d162c6b --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/probe_dispatch_tests.rs @@ -0,0 +1,278 @@ +//! #7850: the header-directed probe dispatch in `gc_pointer_and_type_from_value`. +//! +//! Every dynamic method call goes through that function, and it used to run four +//! side-registry probes — `is_registered_set`, `is_registered_map`, +//! `is_regex_pointer`, `is_registered_symbol` — before reading the `GcHeader` +//! that already records the kind three of them were looking for. The symbol one +//! is the expensive one: a process-global `Mutex` plus a SipHash, entered on +//! every dispatch as soon as ANY `Symbol` exists, which one `for…of` makes true +//! (it materialises `Symbol.iterator`). +//! +//! These tests pin the two halves that a re-ordering can break: +//! +//! * **the saving is real** — a plain-object receiver must not move the symbol, +//! map or set probe counters, *with the symbol latch armed*. A test that only +//! checked "nothing threw" would pass with the whole optimisation deleted; +//! this one goes red (case 4 of CLAUDE.md's "four ways a gate can be unable to +//! fail"). +//! * **the answer is unchanged** — Set, Map, RegExp, fresh `Symbol()` and +//! `Box`-leaked (`Symbol.for` / well-known) receivers must all still be +//! excluded, including when they are created AFTER the idle fast path has +//! already answered for an unrelated address. +//! +//! The leaked symbols are the interesting case and the reason +//! `symbol::may_be_symbol_header` exists: they have no `GcHeader`, so `ptr - 8` +//! is foreign allocator bytes that can read as any `obj_type` at all. The screen +//! is therefore on the object's OWN first word (`SYMBOL_MAGIC`), not on the +//! header. `header_directed_dispatch_needs_the_symbol_magic_screen` sabotages it +//! and requires the classification to go WRONG, so a future edit that drops the +//! screen cannot leave these tests quietly green. +//! +//! Leaked symbols are minted through `Symbol.for` with a key unique to each +//! test rather than through the well-known cache: `WELL_KNOWN_SYMBOLS` is a +//! process-global cache while `SYMBOL_POINTERS` is `per_test_global!` (i.e. per +//! THREAD under `cargo test`), so a well-known symbol first created on another +//! test thread would come back cached and unregistered here. A unique key always +//! allocates and registers on the calling thread. + +use super::*; + +fn nanboxed(ptr: usize) -> f64 { + f64::from_bits(crate::value::js_nanbox_pointer(ptr as i64).to_bits()) +} + +fn plain_object() -> usize { + crate::object::js_object_alloc(0, 4) as usize +} + +/// A `Box`-leaked symbol (no `GcHeader`), registered on THIS thread. Same +/// storage class as `Symbol.iterator` and the Intl fallback symbol. +fn leaked_symbol(key: &str) -> usize { + let key_str = crate::string::js_string_from_str(key); + let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key_str as i64).to_bits()); + let addr = unsafe { crate::value::js_nanbox_get_pointer(crate::symbol::js_symbol_for(key_f64)) } + as usize; + assert!(addr != 0, "test premise: Symbol.for({key}) allocated"); + assert!( + crate::symbol::is_registered_symbol(addr), + "test premise: Symbol.for({key}) is registered on this thread" + ); + addr +} + +fn classify(addr: usize) -> Option<(*const u8, u8)> { + unsafe { test_gc_pointer_and_type_from_value(nanboxed(addr)) } +} + +/// The saving, asserted rather than assumed: with the symbol latch ARMED — the +/// state every realistic program is in — a plain-object dispatch must not enter +/// `is_registered_symbol` at all, nor the map/set registries. +/// +/// Delete the `obj_type` dispatch and this goes red, because the probes run +/// again. +#[test] +fn plain_object_dispatch_probes_no_side_registry() { + // Arm the latch the way ordinary code does. + leaked_symbol("perry-7850-arm-the-latch"); + assert!( + !crate::symbol::test_symbol_latch_is_idle(), + "test premise: creating a symbol must arm SYMBOL_EVER_REGISTERED" + ); + + let obj = plain_object(); + // Warm any lazy state so the measured call below is steady-state. + assert!(classify(obj).is_some()); + + let sym_before = crate::symbol::test_symbol_registry_probe_count(); + let map_before = crate::map::test_map_registry_probe_count(); + let set_before = crate::set::test_set_registry_probe_count(); + + let got = classify(obj); + assert_eq!( + got.map(|(_, t)| t), + Some(crate::gc::GC_TYPE_OBJECT), + "a plain object must classify as GC_TYPE_OBJECT" + ); + + assert_eq!( + crate::symbol::test_symbol_registry_probe_count(), + sym_before, + "a plain-object dispatch must not take the process-global symbol \ + registry mutex — that was 6.5% of `pipeline` (#7850)" + ); + assert_eq!( + crate::map::test_map_registry_probe_count(), + map_before, + "GC_TYPE_OBJECT rules a Map out; the registry must not be consulted" + ); + assert_eq!( + crate::set::test_set_registry_probe_count(), + set_before, + "GC_TYPE_OBJECT rules a Set out; the registry must not be consulted" + ); +} + +/// The answer, unchanged. Each of these kinds resolved to `None` before the +/// re-ordering and must still. +#[test] +fn exotic_receivers_are_still_excluded() { + let set = crate::set::js_set_alloc(4) as usize; + assert!( + classify(set).is_none(), + "a Set must not classify as an object" + ); + + let map = crate::map::js_map_alloc(4) as usize; + assert!( + classify(map).is_none(), + "a Map must not classify as an object" + ); + + // Fresh `Symbol(desc)`: a `gc_malloc(_, GC_TYPE_STRING)` allocation, so the + // header CAN speak for it — but only through the GC_TYPE_STRING arm. + let fresh = unsafe { + crate::value::js_nanbox_get_pointer(crate::symbol::js_symbol_new_empty()) as usize + }; + assert!(fresh != 0, "test premise: Symbol() allocated"); + assert!( + classify(fresh).is_none(), + "a fresh Symbol must not classify as an object" + ); + + // A `Box`-leaked symbol created AFTER the idle fast path has already + // answered for the unrelated addresses above (#7474 shape). + let leaked = leaked_symbol("perry-7850-after-the-fast-path"); + assert!( + classify(leaked).is_none(), + "a leaked symbol created after the idle fast path must still be excluded" + ); + + // The realistic leaked-symbol path — what a `for…of` mints. It carries no + // GcHeader, so only the magic screen can keep it out of the object arms. + let wk = crate::symbol::well_known_symbol("iterator") as usize; + assert!( + unsafe { crate::symbol::may_be_symbol_header(wk as *const u8) }, + "a well-known symbol must carry SYMBOL_MAGIC in its first word; if it \ + does not, `Symbol.iterator.toString()` reads `ptr - 8` as a GcHeader" + ); +} + +/// RegExp is the one exotic kind that genuinely IS a `GC_TYPE_OBJECT` +/// allocation, so it is the reason the GC_TYPE_OBJECT arm still probes. +#[cfg(feature = "regex-engine")] +#[test] +fn regexp_receiver_is_still_excluded() { + let pattern = crate::string::js_string_from_str("a+b"); + let flags = crate::string::js_string_from_str("g"); + let re = crate::regex::js_regexp_new(pattern, flags) as usize; + assert!(re != 0, "test premise: RegExp allocated"); + assert!( + classify(re).is_none(), + "a RegExp is a GC_TYPE_OBJECT allocation and must still be excluded by \ + the regex probe the GC_TYPE_OBJECT arm keeps" + ); +} + +/// Sabotage. The `SYMBOL_MAGIC` screen is what makes the header-directed +/// dispatch sound; with it forced to "maybe", every dispatch pays the registry +/// again — and with it forced OFF entirely a leaked symbol's `ptr - 8` would be +/// read as if it were a real `GcHeader`. This test pins both directions: +/// screen ON ⟹ no probe for a plain object; screen defeated ⟹ the probe returns. +/// If it stops failing when sabotaged, the screen is not load-bearing and every +/// other assertion here is proving nothing. +#[test] +fn header_directed_dispatch_needs_the_symbol_magic_screen() { + // Arm the latch, then confirm the screen is what suppresses the probe. + leaked_symbol("perry-7850-magic-screen"); + let obj = plain_object(); + assert!(classify(obj).is_some()); + + let before = crate::symbol::test_symbol_registry_probe_count(); + assert!(classify(obj).is_some()); + assert_eq!( + crate::symbol::test_symbol_registry_probe_count(), + before, + "screen ON: a plain object must not reach the symbol registry" + ); + + let restore = crate::symbol::test_disable_symbol_magic_screen(true); + let before = crate::symbol::test_symbol_registry_probe_count(); + let answer = classify(obj).map(|(_, t)| t); + let probed = crate::symbol::test_symbol_registry_probe_count() > before; + crate::symbol::test_disable_symbol_magic_screen(restore); + + assert!( + probed, + "sabotage check: with the magic screen defeated the dispatch MUST fall \ + through to `is_registered_symbol` — if it does not, the screen is not \ + what is keeping the fast path fast and this suite is vacuous" + ); + assert_eq!( + answer, + Some(crate::gc::GC_TYPE_OBJECT), + "the slow path must still give the same answer" + ); +} + +/// Every `Box`-leaked symbol must carry `SYMBOL_MAGIC`, and no ordinary GC +/// object may — the first is soundness (a `false` here is a silent wrong +/// answer), the second is the performance invariant that keeps the fast path +/// firing. Both are cheap to check and both have been wrong in this family. +#[test] +fn the_magic_screen_covers_every_symbol_and_no_ordinary_object() { + for i in 0..8 { + let sym = leaked_symbol(&format!("perry-7850-magic-{i}")); + assert!( + unsafe { crate::symbol::may_be_symbol_header(sym as *const u8) }, + "leaked symbol {sym:#x} must carry SYMBOL_MAGIC" + ); + assert!(classify(sym).is_none(), "leaked symbol {sym:#x} excluded"); + } + let fresh = unsafe { + crate::value::js_nanbox_get_pointer(crate::symbol::js_symbol_new_empty()) as usize + }; + assert!( + unsafe { crate::symbol::may_be_symbol_header(fresh as *const u8) }, + "a gc_malloc'd Symbol must carry SYMBOL_MAGIC too" + ); + + // Soundness sabotage, expressed as data rather than a switch: without the + // screen a leaked symbol would be classified by the bytes at `ptr - 8`, + // which belong to the allocator and not to us. This mirrors the production + // `match` DELIBERATELY — it asserts that no other arm covers these, i.e. + // that the screen is the only thing keeping them out. If someone adds an arm + // that does cover them, this mirror goes stale and the assertion below + // fails, which is the right way round. + for i in 0..8 { + let sym = leaked_symbol(&format!("perry-7850-magic-{i}")); + let obj_type = unsafe { + (*((sym as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader)) + .obj_type + }; + let excluded_without_the_screen = match obj_type { + crate::gc::GC_TYPE_SET => crate::set::is_registered_set(sym), + crate::gc::GC_TYPE_MAP => crate::map::is_registered_map(sym), + crate::gc::GC_TYPE_OBJECT => crate::regex::is_regex_pointer(sym as *const u8), + _ => false, + }; + assert!( + !excluded_without_the_screen, + "leaked symbol {sym:#x} (allocator bytes read as obj_type {obj_type}) would \ + be excluded even without the magic screen — the screen is then not \ + load-bearing and this suite is vacuous" + ); + } + + let mut covered = 0usize; + for _ in 0..64 { + let o = plain_object(); + if unsafe { crate::symbol::may_be_symbol_header(o as *const u8) } { + covered += 1; + } + } + assert_eq!( + covered, 0, + "{covered}/64 fresh GC objects read as SYMBOL_MAGIC; the #7850 fast path \ + is not firing and the symbol registry mutex is back on every dispatch" + ); +} diff --git a/crates/perry-runtime/src/symbol.rs b/crates/perry-runtime/src/symbol.rs index c5f0f351e0..85aa965545 100644 --- a/crates/perry-runtime/src/symbol.rs +++ b/crates/perry-runtime/src/symbol.rs @@ -339,6 +339,58 @@ pub fn is_well_known_symbol(ptr: usize) -> bool { static SYMBOL_EVER_REGISTERED: crate::registry_latch::RegistryLatch = crate::registry_latch::RegistryLatch::new(); +/// True when the bytes at `ptr` *could* be a [`SymbolHeader`], i.e. when a +/// classifier that has already ruled a symbol out some other way must still ask +/// the authoritative [`is_registered_symbol`]. +/// +/// #7850. `gc_pointer_and_type_from_value` — on the path of every dynamic method +/// call — cannot use `GcHeader.obj_type` to rule a symbol out, because three of +/// the five registration sites (`well_known_symbol`, +/// `intl_legacy_constructed_symbol`, `js_symbol_for`) are `Box::into_raw`: +/// process-lifetime allocations with **no `GcHeader` at all**, so `ptr - 8` is +/// foreign allocator bytes that can coincidentally equal any `obj_type`. Trusting +/// the header for those is a silent wrong answer. +/// +/// What every symbol DOES have, whatever its storage, is `SYMBOL_MAGIC` in its +/// own first four bytes — `alloc_symbol` and all three `Box` sites set it, and +/// the field is at offset 0 precisely so cheap discrimination is possible. So +/// one 4-byte load of the object the caller is already about to inspect answers +/// "definitely not a symbol" for everything else. +/// +/// The direction of the guarantee is what makes it safe to use as a screen: +/// **`false` is exact** — no symbol reads `false` — while `true` is merely +/// "ask the registry". A non-symbol whose first word happens to equal +/// `SYMBOL_MAGIC` (a `StringHeader` would need `utf16_len == 0x5359_4D42`, i.e. +/// a 2.8 GB string; an `ObjectHeader`'s `object_type` is a small tag) simply +/// pays the old probe and gets the old, correct answer. +/// +/// # Safety +/// `ptr` must be readable for 4 bytes. Every caller is one that already +/// dereferences the allocation (or its `GcHeader` at `ptr - 8`). +#[inline(always)] +pub(crate) unsafe fn may_be_symbol_header(ptr: *const u8) -> bool { + #[cfg(test)] + if TEST_DISABLE_SYMBOL_MAGIC_SCREEN.with(|c| c.get()) { + return true; + } + std::ptr::read_unaligned(ptr as *const u32) == SYMBOL_MAGIC +} + +/// Test-only override that forces [`may_be_symbol_header`] to answer `true` — +/// i.e. removes the screen without deleting it, so a test can show the screen is +/// what makes the fast path fast rather than dead code in front of a probe that +/// would have answered anyway. +#[cfg(test)] +thread_local! { + static TEST_DISABLE_SYMBOL_MAGIC_SCREEN: std::cell::Cell = + const { std::cell::Cell::new(false) }; +} + +#[cfg(test)] +pub(crate) fn test_disable_symbol_magic_screen(disabled: bool) -> bool { + TEST_DISABLE_SYMBOL_MAGIC_SCREEN.with(|c| c.replace(disabled)) +} + pub(crate) fn register_symbol_pointer(ptr: usize) { // Arm before taking the lock, so the entry is never reachable while the // latch still reads idle. @@ -350,6 +402,26 @@ pub(crate) fn register_symbol_pointer(ptr: usize) { guard.as_mut().unwrap().insert(ptr); } +/// Every entry into [`is_registered_symbol`] that got past the latch, i.e. +/// every caller that could not rule a `Symbol` out more cheaply. Twin of +/// `map::TEST_MAP_REGISTRY_PROBES`: #7850's header-directed dispatch in +/// `object::native_call_method` is asserted against this, so "the probe no +/// longer runs on a plain-object dispatch" is a test rather than a claim. +#[cfg(test)] +thread_local! { + static TEST_SYMBOL_REGISTRY_PROBES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn test_symbol_registry_probe_count() -> u64 { + TEST_SYMBOL_REGISTRY_PROBES.with(|c| c.get()) +} + +#[cfg(test)] +pub(crate) fn test_symbol_latch_is_idle() -> bool { + SYMBOL_EVER_REGISTERED.is_idle() +} + // The `%Intl%.[[FallbackSymbol]]` — a single per-realm symbol whose description // is exactly `"IntlLegacyConstructedSymbol"` (no `Symbol.` prefix, so it is // *not* a well-known symbol). It is stashed on the receiver when a legacy Intl @@ -393,6 +465,8 @@ pub fn is_registered_symbol(ptr: usize) -> bool { if SYMBOL_EVER_REGISTERED.is_idle() { return false; } + #[cfg(test)] + TEST_SYMBOL_REGISTRY_PROBES.with(|c| c.set(c.get().wrapping_add(1))); is_registered_symbol_slow(ptr) } From a18286cc4e7a0922614d5fd17170cf13b8789aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Tue, 11 Aug 2026 19:14:57 +0200 Subject: [PATCH 2/2] docs: changelog fragment for #7868 Claude-Session: https://claude.ai/code/session_012B8z92S82sCfqCrVqrFgS2 --- .../7868-header-directed-probe-dispatch.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 changelog.d/7868-header-directed-probe-dispatch.md diff --git a/changelog.d/7868-header-directed-probe-dispatch.md b/changelog.d/7868-header-directed-probe-dispatch.md new file mode 100644 index 0000000000..899577d630 --- /dev/null +++ b/changelog.d/7868-header-directed-probe-dispatch.md @@ -0,0 +1,100 @@ +### `perf(runtime)`: let the GC header pick the side-registry probe on dynamic dispatch (#7850) + +`object::native_call_method::gc_pointer_and_type_from_value` sits on the path of +**every dynamic method call** (`js_native_call_method` → `class_vtable_fast_guard`). +It ran four address-keyed side-registry probes — `set::is_registered_set`, +`map::is_registered_map`, `regex::is_regex_pointer`, `symbol::is_registered_symbol` — +purely to *exclude* object kinds, and only then read the `GcHeader` that already +records the kind three of them were looking for. + +The symbol probe is the expensive one: a process-global `pthread_mutex` plus a SipHash +over a `HashSet`. It already had a `RegistryLatch` and the latch is correct — it +is just **armed by almost every realistic program**, because `well_known_symbol()` is +what materialises `Symbol.iterator` and that is what a `for…of` lowering reaches for. A +latch a program arms in its first loop is not protection; it only moves the cost behind +a branch that is always taken. + +The header now selects the probe, and each implication is enforced by the probe itself, +so this is a re-ordering rather than a new assumption: `is_registered_set` ends in +`obj_type == GC_TYPE_SET`, `is_registered_map` in `GC_TYPE_MAP`, and `is_regex_pointer` +matches the magic of a `gc_malloc(_, GC_TYPE_OBJECT)` allocation (`js_regexp_new` is the +sole `REGEX_POINTERS` insert). A `GC_TYPE_OBJECT` receiver — the overwhelmingly common +case — now consults **one** registry instead of four, and never the symbol mutex. + +#### The hole the header cannot cover, and why the fix is a content check + +`symbol.rs` has five registration sites and they do **not** agree on storage: three of +them (`well_known_symbol`, `intl_legacy_constructed_symbol`, `js_symbol_for`) are +`Box::into_raw`, i.e. process-lifetime allocations with **no `GcHeader` at all**, so +`ptr - 8` is foreign allocator bytes that can read as any `obj_type`. Trusting the header +for those is exactly the #7846 shape — a proof that is true at one site and assumed +everywhere. + +The first attempt screened them by *address*: a monotone `(lo, hi)` window over the +leaked-symbol addresses, `false` exact, two atomic loads. **Its own invariant test +refuted it** — in a full `cargo test` run the window read +`0x56bbcbd0680..=0x5b0100007673` and 64/64 freshly allocated GC objects fell inside it. +One outlier `Box` widens an address range to span the arena, and the fast path silently +stops firing: still sound, worth nothing. An address range over allocator-chosen +addresses is not a screen. + +What every symbol *does* have, whatever its storage, is `SYMBOL_MAGIC` in its own first +four bytes — `alloc_symbol` and all three `Box` sites set it, and the field is at offset +0 precisely so cheap discrimination is possible. `symbol::may_be_symbol_header(ptr)` is +one 4-byte load of the object the caller is already about to inspect. `false` is exact +(no symbol reads `false`); a false `true` merely pays the old probe and gets the old +answer. It cannot be defeated by allocator placement, and it covers GC-heap and leaked +symbols with a single test. + +#### Tests + +* `probe_dispatch_tests::plain_object_dispatch_probes_no_side_registry` asserts the + saving rather than assuming it: with the symbol latch **armed**, a plain-object + dispatch must not move the symbol / map / set probe counters. Delete the `obj_type` + dispatch and it goes red. (New `symbol::TEST_SYMBOL_REGISTRY_PROBES`, the same + `#[cfg(test)]` idiom `map.rs`, `set.rs` and `arguments.rs` already use.) +* `header_directed_dispatch_needs_the_symbol_magic_screen` is a **sabotage** test: with + the screen defeated, the dispatch must fall back into `is_registered_symbol` — and + still give the same answer. A future edit that drops the screen cannot leave the suite + quietly green. +* `the_magic_screen_covers_every_symbol_and_no_ordinary_object` pins both halves: + soundness (every leaked *and* `gc_malloc`'d symbol carries the magic) and the + performance invariant (0/64 fresh GC objects may read as the magic). This is the + assertion that refuted the address-window design. +* `exotic_receivers_are_still_excluded` / `regexp_receiver_is_still_excluded` — the + answer is unchanged for Set / Map / RegExp / fresh `Symbol()` / leaked symbol, + including one created after the idle fast path already ran (#7474 shape). + +#### Measured result: NULL on the current corpus, and why + +Quiet M1 mini, best-of-7, exit-checked, one batched lock window: **21 programs, all +within ±0.7%** — the noise floor. Nothing here is a win and nothing is a regression, and +that includes two shapes written specifically for this change (`dyncall`, a base-typed +polymorphic tree-walk; `dynmix`, a mixed object/array/`Map` receiver loop; both with a +`for…of` so the symbol latch is armed the way a real program arms it). + +The reason is #7852: it removed `pipeline`'s generic-specialization miss, and the +dynamic-dispatch load the probe was riding went with it (`pipeline` 0.483 s → 0.274 s). +#7850's own sizing caveat predicted exactly this. Symbolicated `sample` of +`bench/pipeline_big.ts` confirms it — **zero** samples reach +`gc_pointer_and_type_from_value` on either arm, while all 43 (baseline) / 40 (this +change) `is_registered_symbol_slow` samples come from `js_object_get_field_ic_miss → +get_field_by_name_tail`. The family did not go away; it moved to the property-get +IC-miss path (#7867). + +This lands for the structural property and the assertion that locks it in, not for a +speedup: a plain-object dispatch now touches no side registry, and the probe counter +turns that from a hope into a test the next megamorphic program cannot quietly undo. + +#### Refuted while scoping — #7850 named three sightings, two were already closed + +* `visit_object_static_prototype_slot_mut`'s mutex + SipHash **per traced object** was + fixed by #7859; `prototype_chain.rs:390` already opens with the + `OBJECT_PROTOTYPES_NONEMPTY` gate and carries the `retain.ts` comment the issue quotes. +* `interp`'s `is_registered_set` / `is_registered_map` / `is_arguments_object` are + already latched by #7469 and #7854; `PROFILE-interp-round3.md`'s shares predate both. + +Two follow-ups filed with the profile evidence: **#7865** +(`js_dyn_index_get`/`js_dyn_index_set` probe the Set and Map registries on every dynamic +index access) and **#7867** (`get_field_by_name_tail` probes four registries before +reading the `GcHeader` it then switches on — where the family lives now).