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
10 changes: 10 additions & 0 deletions changelog.d/6846-region-shadow-clear-dedup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Changed

- Region masked-window versioner (`#6794` follow-up (b)): the region fast copies
no longer re-emit a per-statement shadow-slot clear for a numeric local whose
slot was already cleared and whose writes are suppressed — those
`js_shadow_slot_set(slot, 0)` calls are redundant no-ops but each was an
`_tlv_get_addr` thread-local hit (the dominant runtime symbol in bcryptjs
`_encipher`). Cuts the ta_i32 fast copy's shadow-slot clears from one-per-write
to one-per-slot (12→4 on the 16-round `_encipher` shape) with GC roots and
observable behaviour unchanged (validated under forced evacuation).
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ pub(super) fn compile_closure(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ pub(super) fn compile_module_entry(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down Expand Up @@ -1361,6 +1362,7 @@ pub(super) fn compile_module_entry(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ pub(super) fn compile_function(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ pub(super) fn compile_method(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down Expand Up @@ -1472,6 +1473,7 @@ pub(super) fn compile_static_method(
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,17 @@ pub(crate) struct FnCtx<'a> {
/// until the refinement is dropped (`expr::shadow_slot`).
pub masked_region_scalar_locals: std::collections::HashSet<u32>,

/// #6794 follow-up (b): shadow slots that a masked-window region fast copy
/// has already cleared to 0 for a currently-suppressed local. Because
/// `emit_shadow_slot_update_for_expr` skips every write to a local in
/// `masked_region_scalar_locals`, such a slot provably stays 0 for the rest
/// of the suppression window — so every later per-statement clear of it (the
/// `_tlv_get_addr`-heavy `js_shadow_slot_set(slot, 0)` that dominated
/// bcryptjs `_encipher` profiles) is a redundant no-op. `emit_shadow_slot_clear`
/// skips slots in this set; entries are added right after the first clear and
/// removed the moment the local leaves `masked_region_scalar_locals`.
pub suppressed_cleared_shadow_slots: std::collections::HashSet<u32>,

/// #5093: scoped loop-versioning facts for monomorphic class-field loops.
/// Pushed only around the FAST clone of `lower_class_field_versioned_for`
/// (`stmt/loops.rs`): the loop preheader already proved the receiver's
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/expr/shadow_slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ pub(crate) fn emit_shadow_slot_clear(ctx: &mut FnCtx<'_>, slot_idx: u32) {
if ctx.persistent_shadow_slots.contains(&slot_idx) {
return;
}
// #6794 follow-up (b): the slot was already cleared to 0 for a currently
// shadow-suppressed masked-window-region local, and suppression blocks every
// subsequent write to it (`emit_shadow_slot_update_for_expr`), so it provably
// still holds 0 — this clear is a redundant `js_shadow_slot_set(slot, 0)`
// (the `_tlv_get_addr` TLS hit that dominated bcryptjs `_encipher`). Skip it.
if ctx.suppressed_cleared_shadow_slots.contains(&slot_idx) {
return;
}
ctx.block().call_void(
"js_shadow_slot_set",
&[(I32, &slot_idx.to_string()), (I64, "0")],
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-codegen/src/stmt/masked_window_region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,11 @@ fn lower_region_copy(
if !already_cleared {
crate::expr::emit_shadow_slot_clear(ctx, slot_idx);
}
// #6794 (b): the slot now holds 0 and suppression blocks
// every further write to it, so record it — each later
// per-statement clear of this slot is a redundant
// `js_shadow_slot_set(slot, 0)` TLS hit we now skip.
ctx.suppressed_cleared_shadow_slots.insert(slot_idx);
}
}
if privatize && !unset_ids.contains(&id) {
Expand Down Expand Up @@ -600,6 +605,9 @@ fn lower_region_copy(
// it again.
if ctx.masked_region_scalar_locals.remove(&id) {
if let Some(slot_idx) = ctx.shadow_slot_map.get(&id).copied() {
// #6794 (b): suppression ended — later clears of this slot
// are real again, so stop skipping them.
ctx.suppressed_cleared_shadow_slots.remove(&slot_idx);
if let Some(local_slot) = ctx.locals.get(&id).cloned() {
crate::expr::emit_shadow_slot_bind_for_local(ctx, id);
let current = ctx.block().load(DOUBLE, &local_slot);
Expand Down Expand Up @@ -637,6 +645,9 @@ fn lower_region_copy(
for (id, _) in &saved {
ctx.masked_region_scalar_locals.remove(id);
}
// #6794 (b): the redundant-clear skip set is scoped to this copy; drop it so
// the next copy / post-region code emits real clears again.
ctx.suppressed_cleared_shadow_slots.clear();
for (id, original) in saved {
match original {
Some(original) => {
Expand Down
58 changes: 58 additions & 0 deletions test-files/test_gap_region_shadow_clear_gc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// #6794 follow-up (b): the masked-window region fast copies skip redundant
// per-statement shadow-slot clears (a suppressed local's slot provably stays 0).
// This must not lose a GC root. A region-versioned function is alloc-free and
// call-free by construction (that is what makes its body region-matchable), so a
// GC can never fire *inside* its fast copy — the safety of the skip is static
// (suppression blocks every write, so the slot still holds 0). What a runtime
// test CAN guard is the broader interaction: repeatedly entering/leaving region
// functions while the program allocates enough to drive real collections must
// stay byte-identical to Node and survive forced evacuation. The gap harness
// compares this deterministic output against Node.

// Full 16-round Feistel, bcryptjs `_encipher` shape: untyped Int32Array params,
// dynamic-init l/r, and enough statements that it is never inlined (so it keeps
// its own shadow frame and the region versioner runs on the whole round chain).
function encipher(S: any, P: any, lr: any, off: number): number {
let l = lr[off];
let r = lr[off + 1];
l = (l ^ P[0]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[1]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[2]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[3]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[4]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[5]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[6]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[7]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[8]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[9]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[10]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[11]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[12]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[13]) | 0;
l = (l ^ ((((S[(r>>>24)&0xff] + S[256+((r>>>16)&0xff)])|0) ^ S[512+((r>>>8)&0xff)]) + S[768+(r&0xff)]) ^ P[14]) | 0;
r = (r ^ ((((S[(l>>>24)&0xff] + S[256+((l>>>16)&0xff)])|0) ^ S[512+((l>>>8)&0xff)]) + S[768+(l&0xff)]) ^ P[15]) | 0;
l = (l ^ P[16]) | 0;
r = (r ^ P[17]) | 0;
return (l ^ r) | 0;
}

const S = new Int32Array(1024);
for (let i = 0; i < 1024; i++) S[i] = ((i * 2654435761) ^ (i << 28)) | 0;
const P = new Int32Array(18);
for (let i = 0; i < 18; i++) P[i] = (i * 0x9e3779b1) | 0;
const lr = new Int32Array(2);

// Drive the region function repeatedly while allocating garbage every iteration
// (arrays + objects + occasional strings) so the nursery fills and real
// collections run across many enter/leave cycles of `encipher`.
let acc = 0 | 0;
let sink = "";
for (let i = 0; i < 40000; i++) {
lr[0] = acc;
lr[1] = i;
acc = (acc ^ encipher(S, P, lr, 0)) | 0;
const junk = [i, acc, i ^ acc, { a: i, b: acc }]; // heap allocation each iter
if ((i & 4095) === 0) sink = "" + junk[0] + junk[2]; // occasional string build
}
console.log("acc=" + acc);
console.log("sink=" + sink);
Loading