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
7 changes: 7 additions & 0 deletions changelog.d/8413-pre-register-typed-shape-layouts.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 8 additions & 9 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1864,16 +1864,15 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
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
Expand Down
55 changes: 49 additions & 6 deletions crates/perry-codegen/src/codegen/string_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 13 additions & 11 deletions crates/perry-codegen/src/lower_call/new_alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"
);
Expand Down
56 changes: 32 additions & 24 deletions crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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}"
);
}

Expand Down
43 changes: 26 additions & 17 deletions crates/perry-codegen/src/lower_call/typed_shape_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,32 +66,32 @@ 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
/// runtime, per instance:
///
/// 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,
Expand All @@ -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<String, &perry_hir::Class>,
class_keys_globals: &std::collections::HashMap<String, String>,
class_init_chains: &std::collections::HashMap<
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading