diff --git a/changelog.d/8674-array-forwarding-path-compression.md b/changelog.d/8674-array-forwarding-path-compression.md new file mode 100644 index 0000000000..e58a900fc1 --- /dev/null +++ b/changelog.d/8674-array-forwarding-path-compression.md @@ -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. diff --git a/crates/perry-runtime/src/array/header.rs b/crates/perry-runtime/src/array/header.rs index 06d51cbc61..6c21dfeef3 100644 --- a/crates/perry-runtime/src/array/header.rs +++ b/crates/perry-runtime/src/array/header.rs @@ -634,6 +634,7 @@ 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 @@ -641,6 +642,9 @@ pub(crate) fn clean_arr_ptr(arr: *const ArrayHeader) -> *const ArrayHeader { { 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) @@ -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 diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index dffd1d87c0..5871a808de 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -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); } @@ -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; @@ -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 { @@ -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);