From 9aad3b3328c1f9f02f7d79a5c89e6c057aab73a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 19 Aug 2026 19:16:28 +0200 Subject: [PATCH 1/2] perf(gc): pre-register typed shape layouts --- crates/perry-codegen/src/codegen/mod.rs | 17 ++- .../perry-codegen/src/codegen/string_pool.rs | 55 ++++++++-- .../perry-codegen/src/lower_call/new_alloc.rs | 24 +++-- .../src/lower_call/typed_shape_bake_tests.rs | 56 +++++----- .../src/lower_call/typed_shape_init.rs | 43 +++++--- .../src/runtime_decls/strings.rs | 5 + crates/perry-codegen/src/target_layout.rs | 36 +++++-- crates/perry-runtime/src/gc/layout.rs | 40 +++++-- .../src/gc/layout/typed_shape.rs | 102 ++++++++++++++++++ .../layout_trace/declared_at_allocation.rs | 59 ++++++++++ crates/perry-runtime/src/object/shapes.rs | 23 ++++ 11 files changed, 372 insertions(+), 88 deletions(-) diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index c59ad090f9..7a078f5f16 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1864,16 +1864,15 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> let Some(&class_id) = class_ids.get(class_name) else { continue; }; - let typed_intact = - crate::lower_call::typed_shape_init::layout_pointer_free_at_allocation_in( - &class_table, - &class_keys_globals_map, - &class_init_chains_map, - class_name, - field_count, - ); + let typed_layout = crate::lower_call::typed_shape_init::layout_at_allocation_in( + &class_table, + &class_keys_globals_map, + &class_init_chains_map, + class_name, + field_count, + ); let gc_packed = - crate::target_layout::inline_alloc_gc_packed(&triple, field_count, typed_intact); + crate::target_layout::inline_alloc_gc_packed(&triple, field_count, typed_layout); match inits.get(keys_global) { // Two names (an alias) sharing one keys global must agree on // the word module init writes; if they do not, neither may use diff --git a/crates/perry-codegen/src/codegen/string_pool.rs b/crates/perry-codegen/src/codegen/string_pool.rs index fd55ec14d5..56c54968b2 100644 --- a/crates/perry-codegen/src/codegen/string_pool.rs +++ b/crates/perry-codegen/src/codegen/string_pool.rs @@ -424,7 +424,7 @@ pub(super) fn emit_string_pool( // module init; every `new ClassName()` call from then on does a // single global load + inline allocator call (no SHAPE_CACHE // lookup, no js_build_class_keys_array overhead). - for (idx, (global_name, packed, field_count, _raw_mask_words, _pointer_mask_words)) in + for (idx, (global_name, packed, field_count, raw_mask_words, pointer_mask_words)) in class_keys_init_data.iter().enumerate() { chunker.roll_if_full(); @@ -482,11 +482,54 @@ pub(super) fn emit_string_pool( // and writes it into the receiver's shape word at birth. The keys // global is registered first, so the shape record and every future // instance refer to the rooted/rewriteable canonical array. - let shape_id = blk.call( - I32, - "js_object_shape_id_for_keys", - &[(I64, &arr), (I32, &fc_str)], - ); + // #8405: a pointer-bearing layout that is provable at allocation gets + // its own process-global typed ShapeId. Registering the immutable mask + // beside that id here makes `SIDE_MASK | INTACT` a complete header + // image; every later construction can stamp it without calling the + // per-object installer. The class id plus exact masks form the stable + // identity, so a same-keys object with a different representation can + // never alias this descriptor. + const GC_LAYOUT_AND_INTACT_MASK: u64 = 0xD000; + const GC_SIDE_MASK_AND_INTACT: u64 = 0x9000; + let typed_side_mask = + class_header_image_inits + .get(global_name) + .is_some_and(|&(_, packed)| { + ((packed >> 16) & GC_LAYOUT_AND_INTACT_MASK) == GC_SIDE_MASK_AND_INTACT + }); + let shape_id = if typed_side_mask { + let raw_mask_ref = if raw_mask_words.is_empty() { + "null".to_string() + } else { + format!( + "@{}", + crate::typed_shape::raw_f64_mask_global_name_from_keys_global(global_name) + ) + }; + let pointer_mask_ref = format!( + "@{}", + crate::typed_shape::mask_global_name_from_keys_global(global_name) + ); + blk.call( + I32, + "js_gc_typed_shape_id_for_keys", + &[ + (I32, &cid_str), + (I64, &arr), + (I32, &fc_str), + (PTR, &raw_mask_ref), + (I32, &raw_mask_words.len().to_string()), + (PTR, &pointer_mask_ref), + (I32, &pointer_mask_words.len().to_string()), + ], + ) + } else { + blk.call( + I32, + "js_object_shape_id_for_keys", + &[(I64, &arr), (I32, &fc_str)], + ) + }; let shape_global = format!( "@{}", crate::typed_shape::shape_id_global_name_from_keys_global(global_name) diff --git a/crates/perry-codegen/src/lower_call/new_alloc.rs b/crates/perry-codegen/src/lower_call/new_alloc.rs index 8df48bd119..646e2b05fe 100644 --- a/crates/perry-codegen/src/lower_call/new_alloc.rs +++ b/crates/perry-codegen/src/lower_call/new_alloc.rs @@ -456,15 +456,17 @@ fn emit_instance_alloc_inner( // (`layout_set_typed_unknown`), and a constant cannot express "it // depends". Computed here, before `ctx.block()` takes its mutable // borrow. - *typed_layout_baked = super::typed_shape_init::layout_pointer_free_at_allocation( - ctx, - class_name, - field_count, - ); - let typed_intact_bits = if *typed_layout_baked { - GC_OBJ_TYPED_LAYOUT_INTACT - } else { - 0 + let inline_typed_layout = + super::typed_shape_init::layout_at_allocation(ctx, class_name, field_count); + *typed_layout_baked = inline_typed_layout.is_baked(); + let (layout_bits, typed_intact_bits) = match inline_typed_layout { + crate::target_layout::InlineTypedLayout::None => (GC_LAYOUT_POINTER_FREE, 0), + crate::target_layout::InlineTypedLayout::PointerFree => { + (GC_LAYOUT_POINTER_FREE, GC_OBJ_TYPED_LAYOUT_INTACT) + } + crate::target_layout::InlineTypedLayout::SideMask => { + (0x8000, GC_OBJ_TYPED_LAYOUT_INTACT) + } }; let alloc_field_count = std::cmp::max(field_count as u64, MIN_FIELD_SLOTS); @@ -583,13 +585,13 @@ fn emit_instance_alloc_inner( let gc_packed: u64 = crate::target_layout::inline_alloc_gc_packed( ctx.target_triple, field_count, - *typed_layout_baked, + inline_typed_layout, ); debug_assert_eq!( gc_packed, GC_TYPE_OBJECT | (GC_FLAG_ARENA << 8) - | ((GC_LAYOUT_POINTER_FREE | typed_intact_bits) << 16) + | ((layout_bits | typed_intact_bits) << 16) | ((total_size as u64) << 32), "inline_alloc_gc_packed must reproduce this site's packed header word" ); diff --git a/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs b/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs index 286cbe99fa..7e9f4d680f 100644 --- a/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs +++ b/crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs @@ -17,13 +17,13 @@ //! previous tenant's per-object record — behind a `PERRY_PER_OBJECT_LAYOUTS_ANY` //! test whose `0` state proves every thread's tables empty. //! -//! ## What the negative asserts +//! ## Pointer-bearing layouts //! -//! A pointer-BEARING shape keeps the full runtime declare. It has to: its state -//! is `GC_LAYOUT_SIDE_MASK`, which means the collector consults a mask, and the -//! shared `SHAPE_LAYOUTS` descriptor that mask lives in is installed by exactly -//! that call. Baking `SIDE_MASK` into the header without it would hand the -//! tracer a masked object with no mask. +//! #8405 registers their immutable mask once at module init under a dedicated +//! typed ShapeId. That makes `GC_LAYOUT_SIDE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT` +//! complete before the first object is allocated, so this case now drops the +//! per-instance declare too. The test asserts both halves: the one-time typed +//! ShapeId call and the baked header state. //! //! ## Why the pointer-free bake needs no descriptor //! @@ -44,6 +44,7 @@ use perry_hir::{ /// The six-argument per-instance declare this ticket removes. const DECLARE_CALL: &str = "call void @js_gc_declare_typed_shape_layout("; +const TYPED_SHAPE_MINT_CALL: &str = "call i32 @js_gc_typed_shape_id_for_keys("; /// The one-argument address-only remainder that replaces it. const FORGET_CALL: &str = "call void @js_gc_forget_object_layout("; /// The process-global emptiness proof the remainder is gated on. @@ -67,15 +68,14 @@ const ANY_ATOMIC_LOAD: &str = /// and says nothing about what this test is actually for (whether /// `GC_OBJ_TYPED_LAYOUT_INTACT` is claimed), so derive the part that is /// incidental and keep asserting the part that is not. -fn header_word(intact: bool) -> String { +fn header_word(layout_state: u64, intact: bool) -> String { const GC_TYPE_OBJECT: u64 = 0x02; const GC_FLAG_ARENA: u64 = 0x02; - const GC_LAYOUT_POINTER_FREE: u64 = 0x4000; const GC_OBJ_TYPED_LAYOUT_INTACT: u64 = 0x1000; let slots = std::cmp::max(2, crate::target_layout::INLINE_SLOT_FLOOR); let size = 8 + crate::target_layout::object_header_size_bytes("aarch64-apple-darwin") + 8 * slots; - let reserved = GC_LAYOUT_POINTER_FREE + let reserved = layout_state | if intact { GC_OBJ_TYPED_LAYOUT_INTACT } else { @@ -91,11 +91,16 @@ fn header_word(intact: bool) -> String { /// The packed word WITH the baked `GC_OBJ_TYPED_LAYOUT_INTACT`. fn baked_header_word() -> String { - header_word(true) + header_word(0x4000, true) } /// The same word WITHOUT it — what the pointer-bearing class still writes. fn unbaked_header_word() -> String { - header_word(false) + header_word(0x4000, false) +} +/// A registered pointer-bearing class starts in SIDE_MASK with an intact +/// descriptor reachable through its dedicated typed ShapeId. +fn side_mask_baked_header_word() -> String { + header_word(0x8000, true) } fn ir_opts() -> CompileOptions { @@ -400,30 +405,33 @@ fn a_pointer_free_shape_bakes_its_layout_into_the_header_constant() { ); } -/// `class Link { a: number; b: Link | null }` — the control. One declared type -/// differs; everything else about the program is identical. +/// `class Link { a: number; b: Link | null }` — one declared type differs; +/// everything else about the program is identical. #[test] -fn a_pointer_bearing_shape_keeps_the_runtime_declare() { +fn a_pointer_bearing_shape_registers_once_and_bakes_the_side_mask() { let ir = emit(&loop_new_module( "Link", Type::Union(vec![Type::Named("Link".to_string()), Type::Null]), Expr::Null, )); assert!( - ir.contains(DECLARE_CALL), - "a SIDE_MASK shape MUST keep the declare — it is the only thing that \ - installs the shared `SHAPE_LAYOUTS` descriptor the tracer's mask \ - lookup reads:\n{ir}" + !ir.contains(DECLARE_CALL), + "the per-instance declare survived for a pointer-bearing shape:\n{ir}" + ); + assert!( + ir.contains(TYPED_SHAPE_MINT_CALL), + "the pointer mask was not registered at module init:\n{ir}" ); assert!( - ir.contains(&unbaked_header_word()) && !ir.contains(&baked_header_word()), - "the header constant must NOT claim GC_OBJ_TYPED_LAYOUT_INTACT for a \ - shape whose descriptor is installed at runtime:\n{ir}" + ir.contains(&side_mask_baked_header_word()) + && !ir.contains(&unbaked_header_word()) + && !ir.contains(&baked_header_word()), + "the header image does not carry SIDE_MASK | TYPED_LAYOUT_INTACT:\n{ir}" ); assert!( - !ir.contains(FORGET_CALL), - "the standalone forget is only for the baked path; the declare already \ - performs it:\n{ir}" + ir.contains(FORGET_CALL) && ir.contains(ANY_ATOMIC_LOAD), + "the address-dependent stale-record cleanup must survive behind its \ + global emptiness gate:\n{ir}" ); } diff --git a/crates/perry-codegen/src/lower_call/typed_shape_init.rs b/crates/perry-codegen/src/lower_call/typed_shape_init.rs index fcadf53e93..b4cd99aa65 100644 --- a/crates/perry-codegen/src/lower_call/typed_shape_init.rs +++ b/crates/perry-codegen/src/lower_call/typed_shape_init.rs @@ -66,9 +66,8 @@ pub(crate) fn layout_declared_at_allocation_in( }) } -/// #7834: is `class_name`'s at-allocation declaration expressible as a -/// **constant** — the state `GC_LAYOUT_POINTER_FREE | GC_OBJ_TYPED_LAYOUT_INTACT` -/// stamped straight into the inline-bump path's packed `GcHeader` store? +/// Is `class_name`'s at-allocation declaration expressible in the packed +/// `GcHeader` store? /// /// Three conditions, and each maps to one branch of /// `gc::layout::init_typed_shape_layout` that would otherwise decide it at @@ -76,22 +75,23 @@ pub(crate) fn layout_declared_at_allocation_in( /// /// 1. [`layout_declared_at_allocation`] — the declare form is what would have /// been emitted at all, so the fresh-slot proof is already discharged. -/// 2. The pointer mask is **statically empty**, so the state is -/// `GC_LAYOUT_POINTER_FREE` and the shape needs no `SHAPE_LAYOUTS` -/// descriptor: with no pointer-bearing slot there is nothing for a mask to -/// select, and `heap_payload_slot_selection` skips the payload outright. -/// 3. `field_count == slot_count` — the runtime's one *downgrading* branch +/// 2. `field_count == slot_count` — the runtime's one *downgrading* branch /// (`layout_set_typed_unknown`), which a constant cannot express. +/// 3. A pointer-bearing mask is paired with a dedicated typed ShapeId at +/// module init. The runtime registers that ShapeId's exact descriptor once, +/// before the header image is published, so `SIDE_MASK | INTACT` is just as +/// self-contained at allocation as #7834's descriptor-free pointer-free +/// state. /// /// What is deliberately NOT folded in is `layout_forget_object`: it depends on /// the recycled ADDRESS, not on the shape. The caller emits it separately, /// behind the `PERRY_PER_OBJECT_LAYOUTS_ANY` gate. -pub(super) fn layout_pointer_free_at_allocation( +pub(super) fn layout_at_allocation( ctx: &FnCtx<'_>, class_name: &str, field_count: u32, -) -> bool { - layout_pointer_free_at_allocation_in( +) -> crate::target_layout::InlineTypedLayout { + layout_at_allocation_in( ctx.classes, ctx.class_keys_globals, ctx.class_init_chains, @@ -100,9 +100,9 @@ pub(super) fn layout_pointer_free_at_allocation( ) } -/// [`layout_pointer_free_at_allocation`] over the module-level maps (#8122; see +/// [`layout_at_allocation`] over the module-level maps (#8122; see /// [`layout_declared_at_allocation_in`]). -pub(crate) fn layout_pointer_free_at_allocation_in( +pub(crate) fn layout_at_allocation_in( classes: &std::collections::HashMap, class_keys_globals: &std::collections::HashMap, class_init_chains: &std::collections::HashMap< @@ -111,16 +111,25 @@ pub(crate) fn layout_pointer_free_at_allocation_in( >, class_name: &str, field_count: u32, -) -> bool { +) -> crate::target_layout::InlineTypedLayout { + use crate::target_layout::InlineTypedLayout; + if !layout_declared_at_allocation_in(classes, class_keys_globals, class_name) { - return false; + return InlineTypedLayout::None; } let Some(typed_layout) = resolve_typed_layout_in(classes, class_keys_globals, class_init_chains, class_name) else { - return false; + return InlineTypedLayout::None; }; - typed_layout.pointer_mask_words.is_empty() && typed_layout.slot_count == field_count + if typed_layout.slot_count != field_count { + return InlineTypedLayout::None; + } + if typed_layout.pointer_mask_words.is_empty() { + InlineTypedLayout::PointerFree + } else { + InlineTypedLayout::SideMask + } } /// Emit the `js_gc_declare_typed_shape_layout` call that registers a **freshly diff --git a/crates/perry-codegen/src/runtime_decls/strings.rs b/crates/perry-codegen/src/runtime_decls/strings.rs index f8819347a8..8798e21e08 100644 --- a/crates/perry-codegen/src/runtime_decls/strings.rs +++ b/crates/perry-codegen/src/runtime_decls/strings.rs @@ -1044,6 +1044,11 @@ pub fn declare_phase_b_strings(module: &mut LlModule) { ); module.declare_function("js_build_class_keys_array", I64, &[I32, I32, PTR, I32]); module.declare_function("js_object_shape_id_for_keys", I32, &[I64, I32]); + module.declare_function( + "js_gc_typed_shape_id_for_keys", + I32, + &[I32, I64, I32, PTR, I32, PTR, I32], + ); // Inline bump-allocator state accessor + slow path. The codegen // calls `js_inline_arena_state` once per JS function entry, caches // the returned pointer in a stack slot, and reads/writes the diff --git a/crates/perry-codegen/src/target_layout.rs b/crates/perry-codegen/src/target_layout.rs index 6948a290ce..5bf11cb93b 100644 --- a/crates/perry-codegen/src/target_layout.rs +++ b/crates/perry-codegen/src/target_layout.rs @@ -106,10 +106,11 @@ pub(crate) fn inline_alloc_total_size_bytes(target_triple: &str, field_count: u3 /// bits 32..63 = size (u32) inline_alloc_total_size_bytes /// ``` /// -/// `typed_intact` is #7834's bake: when the class's canonical layout is -/// declarable at allocation AND its pointer mask is statically empty, the -/// intact bit is folded into this constant and the per-instance -/// `js_gc_declare_typed_shape_layout` call is skipped. +/// `typed_layout` is the allocation-time bake selected by +/// `lower_call::typed_shape_init`. Pointer-free layouts need no descriptor; +/// pointer-bearing layouts use a module-init ShapeId whose descriptor is +/// registered once for the process, so both can fold their final state into +/// this constant and skip the per-instance layout call. /// /// #8122: ONE definition, shared by the allocation site /// (`lower_call/new_alloc.rs`) and the module-level header-image table @@ -118,10 +119,24 @@ pub(crate) fn inline_alloc_total_size_bytes(target_triple: &str, field_count: u3 /// byte — a divergence would publish objects whose recorded size or layout /// state the collector cannot trust — so the arithmetic lives here and the /// site cross-checks the table's value against its own before using it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum InlineTypedLayout { + None, + PointerFree, + SideMask, +} + +impl InlineTypedLayout { + #[inline] + pub(crate) fn is_baked(self) -> bool { + self != Self::None + } +} + pub(crate) fn inline_alloc_gc_packed( target_triple: &str, field_count: u32, - typed_intact: bool, + typed_layout: InlineTypedLayout, ) -> u64 { const GC_TYPE_OBJECT: u64 = 2; const GC_FLAG_ARENA: u64 = 0x02; @@ -129,18 +144,19 @@ pub(crate) fn inline_alloc_gc_packed( // field-store sites issue per-slot `js_gc_note_slot_layout` so the GC // sees real pointer-bearing slots regardless of this initial tag. const GC_LAYOUT_POINTER_FREE: u64 = 0x4000; + const GC_LAYOUT_SIDE_MASK: u64 = 0x8000; /// `GC_OBJ_TYPED_LAYOUT_INTACT` — the bit `class_field_inline_guard` /// requires before it will read or write a raw-f64 slot directly. /// Runtime-side name: `gc::layout::GC_OBJ_TYPED_LAYOUT_INTACT`. const GC_OBJ_TYPED_LAYOUT_INTACT: u64 = 0x1000; - let typed_intact_bits = if typed_intact { - GC_OBJ_TYPED_LAYOUT_INTACT - } else { - 0 + let reserved = match typed_layout { + InlineTypedLayout::None => GC_LAYOUT_POINTER_FREE, + InlineTypedLayout::PointerFree => GC_LAYOUT_POINTER_FREE | GC_OBJ_TYPED_LAYOUT_INTACT, + InlineTypedLayout::SideMask => GC_LAYOUT_SIDE_MASK | GC_OBJ_TYPED_LAYOUT_INTACT, }; GC_TYPE_OBJECT | (GC_FLAG_ARENA << 8) - | ((GC_LAYOUT_POINTER_FREE | typed_intact_bits) << 16) + | (reserved << 16) | (inline_alloc_total_size_bytes(target_triple, field_count) << 32) } diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index e7ef9059f5..b0746f23f3 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -126,7 +126,9 @@ mod slot_mask; mod typed_shape; pub(in crate::gc) use slot_mask::LayoutSlotMask; -pub use typed_shape::{js_gc_declare_typed_shape_layout, js_gc_init_typed_shape_layout}; +pub use typed_shape::{ + js_gc_declare_typed_shape_layout, js_gc_init_typed_shape_layout, js_gc_typed_shape_id_for_keys, +}; /// What a single store means for the object's canonical typed descriptor. /// Computed while the descriptor is still borrowed, acted on after @@ -181,7 +183,9 @@ fn shape_layout_keyed_enabled() -> bool { use std::sync::OnceLock; static E: OnceLock = OnceLock::new(); // Default ON; `PERRY_SHAPE_LAYOUT_KEYED=0` restores the per-object maps - // (A/B validation). + // for dynamically learned layouts (A/B validation). Codegen-registered, + // immutable layouts remain available because their side-mask headers + // depend on them for correctness. *E.get_or_init(|| super::env_default_on_enabled("PERRY_SHAPE_LAYOUT_KEYED")) } @@ -195,9 +199,6 @@ unsafe fn with_shape_shared_descriptor( user_ptr: usize, f: impl Fn(&TypedLayoutDescriptor) -> R, ) -> Option { - if !shape_layout_keyed_enabled() { - return None; - } // keys_array / ShapeId only exist on genuine shaped objects // (`ObjectFields`). Arrays, closures, RegExps etc. also flow through // `layout_note_slot` / `layout_visit_pointer_slots`, and reading those @@ -232,9 +233,6 @@ unsafe fn with_shape_shared_descriptor_from( descriptor: Option, f: impl Fn(&TypedLayoutDescriptor) -> R, ) -> Option { - if !shape_layout_keyed_enabled() { - return None; - } let object = user_ptr as *const crate::object::ObjectHeader; let shape_id = crate::object::shapes::object_shape_stamp(object); if shape_id == 0 { @@ -246,12 +244,32 @@ unsafe fn with_shape_shared_descriptor_from( let field_count = descriptor .map(|descriptor| descriptor.live_inline_slot_count as usize) .unwrap_or(0); - let map = hot_shape_layouts().borrow(); - let desc = map.get(&shape_id)?.as_ref()?; + if shape_layout_keyed_enabled() { + let map = hot_shape_layouts().borrow(); + if let Some(desc) = map.get(&shape_id) { + let desc = desc.as_ref()?; + if desc.slot_count != field_count { + return None; + } + return Some(f(desc)); + } + } + + // #8405: codegen-registered pointer-bearing class layouts live in a + // process-global immutable registry because the module header image is + // shared by workers. A dedicated ShapeId makes this lookup unambiguous. + // Cache the descriptor in the current agent's ordinary hot table on first + // use, so the mutex is paid once per shape/thread, never per trace/store. + let desc = typed_shape::registered_typed_shape_layout(shape_id)?; if desc.slot_count != field_count { return None; } - Some(f(desc)) + if shape_layout_keyed_enabled() { + hot_shape_layouts() + .borrow_mut() + .insert(shape_id, Some(desc.clone())); + } + Some(f(&desc)) } /// Answer a *query* about `user_ptr`'s current canonical typed layout, whichever diff --git a/crates/perry-runtime/src/gc/layout/typed_shape.rs b/crates/perry-runtime/src/gc/layout/typed_shape.rs index 27124d5379..3254b6b2c6 100644 --- a/crates/perry-runtime/src/gc/layout/typed_shape.rs +++ b/crates/perry-runtime/src/gc/layout/typed_shape.rs @@ -59,6 +59,108 @@ unsafe fn mask_words<'a>(words: *const u64, word_count: u32) -> &'a [u64] { } } +/// Process-global typed class layouts installed by codegen at module init. +/// +/// Ordinary `SHAPE_LAYOUTS` entries are agent-local because they are learned +/// from objects at runtime. These entries describe immutable code-image masks +/// and use a dedicated ShapeId, so the descriptor is valid in every worker and +/// can be copied into that worker's hot table on first use. +#[derive(Clone, Hash, PartialEq, Eq)] +struct RegisteredTypedShapeKey { + class_id: u32, + slot_count: u32, + raw_f64_words: Vec, + pointer_words: Vec, +} + +#[derive(Default)] +struct RegisteredTypedShapes { + ids_by_layout: std::collections::HashMap, + layouts_by_id: std::collections::HashMap, +} + +static REGISTERED_TYPED_SHAPES: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(RegisteredTypedShapes::default())); + +fn registered_typed_shapes() -> std::sync::MutexGuard<'static, RegisteredTypedShapes> { + REGISTERED_TYPED_SHAPES + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Copy a module-init descriptor for `shape_id`, if this is one of #8405's +/// dedicated typed class shapes. The caller installs the copy in its +/// agent-local hot table, making the global mutex a once-per-shape/thread cold +/// path rather than a trace/store cost. +pub(super) fn registered_typed_shape_layout(shape_id: u32) -> Option { + registered_typed_shapes() + .layouts_by_id + .get(&shape_id) + .cloned() +} + +/// Mint (or reuse) a ShapeId whose identity includes the exact typed layout. +/// Called once per eligible class at module initialization, before its header +/// image is published. Every allocation can therefore stamp +/// `SIDE_MASK | TYPED_LAYOUT_INTACT` without a per-object runtime call. +#[no_mangle] +pub extern "C" fn js_gc_typed_shape_id_for_keys( + class_id: u32, + keys: u64, + slot_count: u32, + raw_f64_words: *const u64, + raw_f64_word_count: u32, + pointer_words: *const u64, + pointer_word_count: u32, +) -> u32 { + if class_id == 0 || keys == 0 || slot_count >= 16_000_000 { + eprintln!("Perry internal error: invalid pre-registered typed shape"); + std::process::abort(); + } + let (raw_f64_slice, pointer_slice) = unsafe { + ( + mask_words(raw_f64_words, raw_f64_word_count), + mask_words(pointer_words, pointer_word_count), + ) + }; + if pointer_slice.is_empty() + || shape_install::words_intersect(raw_f64_slice, pointer_slice, slot_count as usize) + { + eprintln!("Perry internal error: invalid pre-registered typed shape masks"); + std::process::abort(); + } + let key = RegisteredTypedShapeKey { + class_id, + slot_count, + raw_f64_words: raw_f64_slice.to_vec(), + pointer_words: pointer_slice.to_vec(), + }; + let descriptor = TypedLayoutDescriptor { + slot_count: slot_count as usize, + raw_f64_mask: LayoutSlotMask::from_words(raw_f64_slice), + pointer_mask: LayoutSlotMask::from_words(pointer_slice), + }; + let mut registered = registered_typed_shapes(); + if let Some(&shape_id) = registered.ids_by_layout.get(&key) { + if !crate::object::shapes::install_registered_typed_shape_id( + shape_id, + keys as usize as *const crate::array::ArrayHeader, + slot_count, + ) { + eprintln!("Perry internal error: typed ShapeId structural mismatch"); + std::process::abort(); + } + return shape_id; + } + let shape_id = crate::object::shapes::mint_registered_typed_shape_id( + keys as usize as *const crate::array::ArrayHeader, + slot_count, + ); + registered.ids_by_layout.insert(key, shape_id); + registered.layouts_by_id.insert(shape_id, descriptor); + shape_id +} + #[allow(clippy::too_many_arguments)] unsafe fn init_typed_shape_layout( user_ptr: usize, diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs b/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs index 52348a97cd..d6a3cc0e51 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/declared_at_allocation.rs @@ -190,3 +190,62 @@ fn test_declaring_install_rejects_overlapping_masks() { clear_marks(); clear_mark_seeds(); } + +/// #8405: codegen can register a pointer-bearing class descriptor once at +/// module init and stamp every newborn's final header state. The object must be +/// traceable without ever calling either per-object typed-layout entry point. +#[test] +fn test_registered_typed_shape_traces_without_a_per_object_install() { + clear_marks(); + clear_mark_seeds(); + + let class_id = 17; + let packed = b"peer\0payload\0"; + let keys = + crate::object::js_build_class_keys_array(class_id, 2, packed.as_ptr(), packed.len() as u32); + let raw_mask = [0b10u64]; + let pointer_mask = [0b01u64]; + let shape_id = js_gc_typed_shape_id_for_keys( + class_id, + keys as usize as u64, + 2, + raw_mask.as_ptr(), + 1, + pointer_mask.as_ptr(), + 1, + ); + let obj = + crate::object::js_object_alloc_class_inline_keys_stamped(class_id, 0, 2, keys, shape_id); + let child = crate::string::js_string_from_bytes(b"registered".as_ptr(), 10); + unsafe { + let header = header_from_user_ptr(obj as *const u8); + header_set_typed_layout_intact(header); + set_layout_state(header, GC_LAYOUT_SIDE_MASK); + let fields = + (obj as *mut u8).add(std::mem::size_of::()) as *mut u64; + *fields = STRING_TAG | (child as u64 & POINTER_MASK); + *fields.add(1) = 1.5f64.to_bits(); + } + + assert!(layout_typed_intact_for_user(obj as usize)); + assert!(layout_typed_raw_f64_slot_for_user(obj as usize, 1)); + assert_eq!(test_layout_pointer_slot_count(obj as usize, 2), Some(1)); + + let child_header = unsafe { header_from_user_ptr(child as *const u8) }; + let valid_ptrs = build_valid_pointer_set(); + assert!(try_mark_value( + POINTER_TAG | (obj as u64 & POINTER_MASK), + &valid_ptrs + )); + trace_marked_objects(&valid_ptrs); + unsafe { + assert_ne!( + (*child_header).gc_flags & GC_FLAG_MARKED, + 0, + "the registered SIDE_MASK must lead the tracer to its pointer slot" + ); + } + + clear_marks(); + clear_mark_seeds(); +} diff --git a/crates/perry-runtime/src/object/shapes.rs b/crates/perry-runtime/src/object/shapes.rs index c7f3a03046..2f64a231f5 100644 --- a/crates/perry-runtime/src/object/shapes.rs +++ b/crates/perry-runtime/src/object/shapes.rs @@ -508,6 +508,29 @@ pub extern "C" fn js_object_shape_id_for_keys(keys: u64, key_count: u32) -> u32 shape_id_for_keys_ensure(keys as usize as *const ArrayHeader, key_count) } +/// Mint a process-global ShapeId for a codegen-registered typed layout and +/// install its structural descriptor in the current agent. Unlike +/// [`shape_id_for_keys_ensure`], this deliberately does not canonicalise by +/// keys alone: two objects with identical property names but different raw +/// slot representations must never share a pre-baked GC descriptor. +pub(crate) fn mint_registered_typed_shape_id(keys: *const ArrayHeader, key_count: u32) -> u32 { + let id = alloc_shape_id().unwrap_or_else(|_| shape_id_exhausted_abort()); + if !install_external_shape_id(id, keys, key_count, key_count) { + invalid_shape_facts_abort(); + } + id +} + +/// Install an already-minted process-global typed ShapeId in this agent (for +/// another module or worker that reuses the same compiled class identity). +pub(crate) fn install_registered_typed_shape_id( + id: u32, + keys: *const ArrayHeader, + key_count: u32, +) -> bool { + install_external_shape_id(id, keys, key_count, key_count) +} + /// Install a process-global id into this agent's local descriptor table. /// Module globals are initialized once per process, while workers own distinct /// runtime state and moving keys pointers. Global id uniqueness makes a local From 8eab6b784a9d3fcd9684a5bf2203deb9d99024b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 19 Aug 2026 19:18:15 +0200 Subject: [PATCH 2/2] docs: add PR 8413 changelog fragment --- changelog.d/8413-pre-register-typed-shape-layouts.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog.d/8413-pre-register-typed-shape-layouts.md diff --git a/changelog.d/8413-pre-register-typed-shape-layouts.md b/changelog.d/8413-pre-register-typed-shape-layouts.md new file mode 100644 index 0000000000..80603624af --- /dev/null +++ b/changelog.d/8413-pre-register-typed-shape-layouts.md @@ -0,0 +1,7 @@ +### Performance + +- Pre-register immutable pointer-bearing class layouts at module initialization + and bake their final side-mask state into inline allocation headers. Eligible + allocations no longer call the typed-layout installer per object, reducing + retired instructions by 23.9% on `cycles`, 8.3% on `interp`, and 8.1% on + `iso_miss`, while preserving byte-exact output across the 19-program corpus.