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
48 changes: 48 additions & 0 deletions changelog.d/7369-gc-leaf-register-global-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
### Changed

**Two runtime helpers admitted to the GC-effect allowlist — and a measured null result on binary size.**

`js_gc_register_global_root` was the single most frequent non-leaf callee in the
probe suite (148 call sites), and it is provably GC-leaf. Its entire body is:

```rust
runtime_write_barrier_root_heap_word(*root); // shade one header
GLOBAL_ROOTS.with(|r| r.borrow_mut().push(root)); // TLS Vec push
```

The first call is exactly what `js_write_barrier_root_heap_word` — already
`CannotCollect` — wraps in one line. The second is a `Vec::push`, and the
"malloc count threshold" GC trigger does not apply to it: that counter is
`MALLOC_STATE.objects.len()`, a registry of Perry GC objects, and the
`#[global_allocator]` is plain mimalloc/System with no GC hook.
`js_typed_feedback_maybe_dump_trace` joins its already-admitted family siblings
(env read, JSON serialise, file write; empty body without `diagnostics`).

**The result, measured A/B on the same tree, is that this buys nothing:**

| probe | safepoints | roots | total bytes | `__text` |
|---|---:|---:|---:|---:|
| `06_string_retention` | 105 → 100 | 27 → 27 | 0 | −4 B |
| `09_try_catch_roots` | 343 → 339 | 259 → 259 | 0 | −4 B |
| `11_collect_at_depth` | 120 → 117 | 36 → 36 | 0 | −4 B |

Root counts are **identical**. The 40 safepoints removed across the suite were
all rootless, and a safepoint with no live roots costs essentially nothing —
which is precisely what `docs/engine-plan.md` already says ("Statepoints have no
fixed cost… the axis is not 'statepoints are bigger', it is 'roots are bigger'").

This is worth recording as evidence rather than a win: **the safepoint-count
lever is not the binary-size lever.** Sequencing step 2's "reduce root density"
must attack live-root *sets*, not safepoint counts. Anyone reaching for the next
obvious helper should read the second test below first.

Two tests come with it. `register_global_root_tracks_the_barrier_it_wraps` pins
the two classifications together so a future demotion of the barrier cannot
leave its wrapper claiming to be leaf. `allocating_helpers_are_not_cannot_collect`
pins `js_nanbox_string` **out** of the allowlist: at 120 call sites it is the
obvious next candidate and it reads as pure bit manipulation, but its
null-pointer guard calls `js_string_from_bytes` to allocate an empty string
rather than boxing null.

Probe suite: 11/11 byte-identical to the Node oracle under `PERRY_RS4GC=1
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1`.
290 changes: 2 additions & 288 deletions crates/perry-codegen/src/expr/index_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ use crate::native_value::{
NativeRep, SemanticKind,
};
use crate::type_analysis::{is_array_expr, is_numeric_expr, is_string_expr, receiver_class_name};
use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8};
use crate::types::{DOUBLE, I32, I64};

use super::index_set_typed_array::lower_inline_dyn_typed_array_set;
use super::{
array_kind_fact, array_store_needs_layout_note, array_store_needs_write_barrier,
buffer_access_materialization_reason, emit_array_numeric_write_note_on_block,
Expand Down Expand Up @@ -1746,290 +1747,3 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
_ => unreachable!("expr/mod.rs dispatched a variant not handled by this submodule"),
}
}

/// #5525 follow-up: guarded **inline** typed-array element STORE for an
/// `obj[i] = v` whose receiver static type is erased (`any`/unknown) but is, at
/// runtime, commonly an owning numeric typed array (bcryptjs's `P[i]=`/`S[i]=`
/// Int32Array boxes). Mirrors [`index_get::lower_inline_dyn_typed_array_get`]:
/// the same pointer / `PERRY_TA_VIEW_GUARD` / `PERRY_TA_KIND_CACHE` / index
/// guards, then a direct per-kind store into `header + 16 + idx*elem_size`,
/// falling back to `js_dyn_index_set` on any guard miss. The store result is the
/// assigned value (`val_double`), matching `js_dyn_index_set`'s return.
///
/// Only the kinds with a simple ToInt32/ToUint32 truncating store (Int8/Uint8/
/// Int16/Uint16/Int32/Uint32) or a direct float store (Float32/Float64) are
/// inlined — i.e. `kind <= KIND_FLOAT64` (7). Uint8ClampedArray (round-half-to-
/// even clamp), the BigInt kinds (ToBigInt / throw) and Float16 (f16 encode) are
/// excluded by the guard and defer to the runtime, which already owns them. The
/// integer truncation here (`toint32(value)` then narrow) is bit-identical to
/// the runtime `store_at`'s `to_uint32_bits(value) as <width>`; the float store
/// is identical to `store_at`'s direct slot write — so behavior matches the
/// existing runtime fast path exactly.
fn lower_inline_dyn_typed_array_set(
ctx: &mut FnCtx<'_>,
obj_box: &str,
idx_d: &str,
val_double: &str,
) -> String {
let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK);
let pointer_tag = crate::nanbox::POINTER_TAG_I64;
let pointer_mask = crate::nanbox::POINTER_MASK_I64;

let fast_idx = ctx.new_block("tav.set.fast");
let store_idx = ctx.new_block("tav.set.store");
let slow_idx = ctx.new_block("tav.set.slow");
let merge_idx = ctx.new_block("tav.set.merge");
let fast_label = ctx.block_label(fast_idx);
let store_label = ctx.block_label(store_idx);
let slow_label = ctx.block_label(slow_idx);
let merge_label = ctx.block_label(merge_idx);

// ---- entry: combined cache/kind/range guard -> fast | slow ----
let entry_guard = {
let blk = ctx.block();
let obj_bits = blk.bitcast_double_to_i64(obj_box);
let raw = blk.and(I64, &obj_bits, pointer_mask);
let tagged = blk.and(I64, &obj_bits, &tag_mask);
let is_ptr = blk.icmp_eq(I64, &tagged, pointer_tag);
let vg = blk.load(I64, "@PERRY_TA_VIEW_GUARD");
let vg_zero = blk.icmp_eq(I64, &vg, "0");
let slot = blk.lshr(I64, &raw, "3");
let slot = blk.and(I64, &slot, "63");
let entry_ptr = blk.gep(
"[64 x i64]",
"@PERRY_TA_KIND_CACHE",
&[(I64, "0"), (I64, &slot)],
);
let entry_val = blk.load(I64, &entry_ptr);
let entry_addr = blk.lshr(I64, &entry_val, "8");
let addr_match = blk.icmp_eq(I64, &entry_addr, &raw);
let kind = blk.and(I64, &entry_val, "255");
// Stores inline only kinds with a trivial truncating/float store:
// kind <= KIND_FLOAT64 (7). Uint8Clamped (8), BigInt (9/10), Float16
// (11), and the 0xFF sentinel all defer to the runtime.
let kind_ok = blk.icmp_ule(I64, &kind, "7");
let idx_ge0 = blk.fcmp("oge", idx_d, "0.0");
let idx_lt = blk.fcmp("olt", idx_d, "4294967296.0");
let g = blk.and(I1, &is_ptr, &vg_zero);
let g = blk.and(I1, &g, &addr_match);
let g = blk.and(I1, &g, &kind_ok);
let g = blk.and(I1, &g, &idx_ge0);
blk.and(I1, &g, &idx_lt)
};
ctx.block().cond_br(&entry_guard, &fast_label, &slow_label);

// ---- fast: validate integer index + bounds -> store | slow ----
ctx.current_block = fast_idx;
let (raw, idx_i64, kind) = {
let blk = ctx.block();
let obj_bits = blk.bitcast_double_to_i64(obj_box);
let raw = blk.and(I64, &obj_bits, pointer_mask);
let slot = blk.lshr(I64, &raw, "3");
let slot = blk.and(I64, &slot, "63");
let entry_ptr = blk.gep(
"[64 x i64]",
"@PERRY_TA_KIND_CACHE",
&[(I64, "0"), (I64, &slot)],
);
let entry_val = blk.load(I64, &entry_ptr);
let kind = blk.and(I64, &entry_val, "255");
let idx_i64 = blk.fptosi(DOUBLE, idx_d, I64);
(raw, idx_i64, kind)
};
let fast_ok = {
let blk = ctx.block();
let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE);
let is_int = blk.fcmp("oeq", &idx_back, idx_d);
let hdr_ptr = blk.inttoptr(I64, &raw);
let len = blk.load(I32, &hdr_ptr);
let len_i64 = blk.zext(I32, &len, I64);
let in_bounds = blk.icmp_ult(I64, &idx_i64, &len_i64);
blk.and(I1, &is_int, &in_bounds)
};
ctx.block().cond_br(&fast_ok, &store_label, &slow_label);

// ---- store: per-kind direct element store (data = header + 16) ----
ctx.current_block = store_idx;
let data_base = {
let blk = ctx.block();
blk.add(I64, &raw, "16")
};
// ToInt32 of the value once (shared by all integer kinds). For float kinds
// we use the raw double directly. `toint32` matches the runtime
// `to_uint32_bits` (NaN/±Inf/±0 → 0, else trunc-toward-zero mod 2^32).
let val_i32 = ctx.block().toint32(val_double);

let b_i8 = ctx.new_block("tav.s.i8");
let b_u8 = ctx.new_block("tav.s.u8");
let b_i16 = ctx.new_block("tav.s.i16");
let b_u16 = ctx.new_block("tav.s.u16");
let b_i32 = ctx.new_block("tav.s.i32");
let b_u32 = ctx.new_block("tav.s.u32");
let b_f32 = ctx.new_block("tav.s.f32");
let b_f64 = ctx.new_block("tav.s.f64");
let l_i8 = ctx.block_label(b_i8);
let l_u8 = ctx.block_label(b_u8);
let l_i16 = ctx.block_label(b_i16);
let l_u16 = ctx.block_label(b_u16);
let l_i32 = ctx.block_label(b_i32);
let l_u32 = ctx.block_label(b_u32);
let l_f32 = ctx.block_label(b_f32);
let l_f64 = ctx.block_label(b_f64);

// Dispatch chain on `kind` (in the store block, after data_base/val_i32).
let chk = |ctx: &mut FnCtx<'_>, k: &str, hit: &str, next_idx: usize| {
let next_label = ctx.block_label(next_idx);
let cond = ctx.block().icmp_eq(I64, &kind, k);
ctx.block().cond_br(&cond, hit, &next_label);
};
let c1 = ctx.new_block("tav.sd1");
let c2 = ctx.new_block("tav.sd2");
let c3 = ctx.new_block("tav.sd3");
let c4 = ctx.new_block("tav.sd4");
let c5 = ctx.new_block("tav.sd5");
let c6 = ctx.new_block("tav.sd6");
chk(ctx, "0", &l_i8, c1);
ctx.current_block = c1;
chk(ctx, "1", &l_u8, c2);
ctx.current_block = c2;
chk(ctx, "2", &l_i16, c3);
ctx.current_block = c3;
chk(ctx, "3", &l_u16, c4);
ctx.current_block = c4;
chk(ctx, "4", &l_i32, c5);
ctx.current_block = c5;
chk(ctx, "5", &l_u32, c6);
ctx.current_block = c6;
// remaining: kind 6 → f32, else (7) → f64.
let is_f32 = ctx.block().icmp_eq(I64, &kind, "6");
ctx.block().cond_br(&is_f32, &l_f32, &l_f64);

// Per-kind stores. Each: off = idx << shift; addr = data_base + off;
// store narrowed value; br merge.
emit_inline_ta_int_store(
ctx,
b_i8,
&idx_i64,
&data_base,
&merge_label,
"0",
&val_i32,
I8,
);
emit_inline_ta_int_store(
ctx,
b_u8,
&idx_i64,
&data_base,
&merge_label,
"0",
&val_i32,
I8,
);
emit_inline_ta_int_store(
ctx,
b_i16,
&idx_i64,
&data_base,
&merge_label,
"1",
&val_i32,
I16,
);
emit_inline_ta_int_store(
ctx,
b_u16,
&idx_i64,
&data_base,
&merge_label,
"1",
&val_i32,
I16,
);
emit_inline_ta_int_store(
ctx,
b_i32,
&idx_i64,
&data_base,
&merge_label,
"2",
&val_i32,
I32,
);
emit_inline_ta_int_store(
ctx,
b_u32,
&idx_i64,
&data_base,
&merge_label,
"2",
&val_i32,
I32,
);
// F32: fptrunc the double to float, store.
{
ctx.current_block = b_f32;
let blk = ctx.block();
let off = blk.shl(I64, &idx_i64, "2");
let addr = blk.add(I64, &data_base, &off);
let ptr = blk.inttoptr(I64, &addr);
let f = blk.fptrunc(DOUBLE, val_double, F32);
blk.store(F32, &f, &ptr);
blk.br(&merge_label);
}
// F64: store the double raw.
{
ctx.current_block = b_f64;
let blk = ctx.block();
let off = blk.shl(I64, &idx_i64, "3");
let addr = blk.add(I64, &data_base, &off);
let ptr = blk.inttoptr(I64, &addr);
blk.store(DOUBLE, val_double, &ptr);
blk.br(&merge_label);
}

// ---- slow: the unchanged runtime setter ----
ctx.current_block = slow_idx;
ctx.block().call(
DOUBLE,
"js_dyn_index_set",
&[(DOUBLE, obj_box), (DOUBLE, idx_d), (DOUBLE, val_double)],
);
ctx.block().br(&merge_label);

// ---- merge: assignment yields the stored value on every path ----
ctx.current_block = merge_idx;
// All paths produce `val_double` as the expression result (matching
// `js_dyn_index_set`'s `return value`), so no phi is needed.
val_double.to_string()
}

/// Emit one per-kind integer typed-array element store block for
/// [`lower_inline_dyn_typed_array_set`]: switches to `blk_idx`, computes the
/// element address (`data_base + (idx << shift)`), narrows the shared
/// ToInt32-coerced `val_i32` to `elem_ty`, stores it, and branches to
/// `merge_label`.
#[allow(clippy::too_many_arguments)]
fn emit_inline_ta_int_store(
ctx: &mut FnCtx<'_>,
blk_idx: usize,
idx_i64: &str,
data_base: &str,
merge_label: &str,
shift: &str,
val_i32: &str,
elem_ty: crate::types::LlvmType,
) {
ctx.current_block = blk_idx;
let blk = ctx.block();
let off = blk.shl(I64, idx_i64, shift);
let addr = blk.add(I64, data_base, &off);
let ptr = blk.inttoptr(I64, &addr);
let narrowed = if elem_ty == I32 {
val_i32.to_string()
} else {
blk.trunc(I32, val_i32, elem_ty)
};
blk.store(elem_ty, &narrowed, &ptr);
blk.br(merge_label);
}
Loading
Loading