Skip to content
Closed
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/8674-array-forwarding-path-compression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Performance

- Compress validated multi-hop array-growth forwarding chains at their retained
head. Generated indexed-access guards can now heal stale aliases after repeated
capacity growth with their existing one-hop path instead of entering generic
indexed lookup on every element access. On the full `codehz/ecs` suite, 11
alternating Mac mini pairs reduced the 10k read-only query by 81.94% and the
accumulation query by 76.59%, with 11/11 wins and every semantic oracle passing.
Direct Node comparisons still leave 6.282x and 3.127x gaps respectively, so
this change does not claim Node parity.
21 changes: 21 additions & 0 deletions crates/perry-runtime/src/array/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,13 +634,17 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader {
unsafe { crate::value::addr_class::try_read_tracked_gc_header(cleaned as usize) };
unsafe {
let mut steps = 0u32;
let mut first_forwarded_header: *mut crate::gc::GcHeader = std::ptr::null_mut();
while let Some(gc_header) = tracked_header {
let gc_header = gc_header.as_ptr();
if (*gc_header).obj_type != crate::gc::GC_TYPE_ARRAY
|| (*gc_header).gc_flags & crate::gc::GC_FLAG_FORWARDED == 0
{
break;
}
if steps == 0 {
first_forwarded_header = gc_header;
}
let new_user = crate::gc::forwarding_address(gc_header) as usize;
let Some(target_header) =
crate::value::addr_class::try_read_tracked_gc_header(new_user)
Expand All @@ -657,6 +661,23 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader {
return std::ptr::null();
}
}
// A receiver stored through an alias can keep its original array head
// across several capacity-crossing grows. Generated array guards heal
// one forwarding edge inline; without compression, a two-or-more-edge
// chain therefore rejects the guard on every element access and pays
// this entire resolver repeatedly. Once the validated walk reaches the
// live array, point the original retained stub straight at that head.
// The next generated access can then heal its single edge inline.
//
// Do this only after walking and validating the complete chain. A
// corrupt target/cycle returns above and must not rewrite a stub with
// an address that has not been proved to be a tracked array.
if steps > 1 && !first_forwarded_header.is_null() {
crate::gc::set_forwarding_address(
first_forwarded_header,
cleaned as *mut ArrayHeader as *mut u8,
);
}
}
// Issue #179 Phase 2: lazy arrays have a GcHeader with
// obj_type == GC_TYPE_LAZY_ARRAY. Their layout's first two u32s
Expand Down
44 changes: 40 additions & 4 deletions crates/perry-runtime/src/array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,8 @@ fn stale_array_reference_survives_three_growths_and_forced_minor_gc() {
let mut head = initial;

// Capacity progresses 16 -> 32 -> 64 -> 128. Keeping `initial` unchanged
// makes every assertion exercise the complete three-stub chain.
// makes the first resolution exercise the complete three-stub chain; that
// resolution then compresses `initial` directly to the current head.
for i in 0..65u32 {
head = js_array_push_f64(head, i as f64);
}
Expand All @@ -561,8 +562,9 @@ fn stale_array_reference_survives_three_growths_and_forced_minor_gc() {
}

// Root the current head, not the deliberately stale first allocation: the
// handle must prove that evacuation moved the live array itself, while
// `initial` independently exercises the three growth stubs afterward.
// handle must prove that evacuation moved the live array itself. The
// compressed `initial` stub then follows the evacuation edge to that new
// head and remains usable.
let scope = crate::gc::RuntimeHandleScope::new();
let root = scope.root_raw_mut_ptr(head);
let pre_gc_head = head;
Expand All @@ -582,7 +584,7 @@ fn stale_array_reference_survives_three_growths_and_forced_minor_gc() {
assert_eq!(
clean_arr_ptr_mut(initial),
rooted_head,
"the stale three-stub chain must resolve to the relocated rooted head"
"the compressed growth stub must resolve to the relocated rooted head"
);
assert_eq!(js_array_length(initial), 65);
for i in 0..65u32 {
Expand Down Expand Up @@ -688,6 +690,40 @@ fn clean_arr_ptr_rejects_forwarding_cycle() {
}
}

#[test]
fn clean_arr_ptr_compresses_multi_hop_forwarding_chain() {
let first = js_array_alloc(0);
let second = js_array_alloc(0);
let live = js_array_alloc(0);
unsafe {
let first_header = crate::value::addr_class::try_read_tracked_gc_header(first as usize)
.unwrap()
.as_ptr();
let second_header = crate::value::addr_class::try_read_tracked_gc_header(second as usize)
.unwrap()
.as_ptr();
let first_flags = (*first_header).gc_flags;
let second_flags = (*second_header).gc_flags;
let first_payload = *(first as *const u64);
let second_payload = *(second as *const u64);
crate::gc::set_forwarding_address(first_header, second as *mut u8);
crate::gc::set_forwarding_address(second_header, live as *mut u8);

let resolved = clean_arr_ptr(first);
let compressed_target = crate::gc::forwarding_address(first_header);

*(first as *mut u64) = first_payload;
*(second as *mut u64) = second_payload;
(*first_header).gc_flags = first_flags;
(*second_header).gc_flags = second_flags;
assert_eq!(resolved, live);
assert_eq!(
compressed_target, live as *mut u8,
"the original stub must point directly at the validated live head"
);
}
}

#[test]
fn clean_arr_ptr_rejects_untracked_forwarding_target_without_deref() {
let array = js_array_alloc(0);
Expand Down
Loading