From a1e82dd68e1fda23ac89c26a6236897e71268f45 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 12:48:31 -0400 Subject: [PATCH 01/12] Pre-register retain cursor audit --- TODO.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/TODO.md b/TODO.md index 3ab586b..c212e8c 100644 --- a/TODO.md +++ b/TODO.md @@ -208,6 +208,48 @@ header alignment without a new real-workload counterexample. - Decision: publish the validated pair, platform-specific graphics, and combined non-pooled report. The next credible implementation audit remains `retain`. +### Retain cursor and publication audit (`perf/retain-cursor-audit`) + +- Status: in progress; pre-registered before committing the candidate +- Baseline: this ledger-only commit, whose `src/lib.rs` is identical to canonical + merge commit `323ae2f`. +- Observation: the canonical Linux matrix measures `retain_mixed/u64` at 1.234x + Vec and the 64-byte workload at 1.113x. The paired macOS matrix measures 1.150x + and 1.070x. Both platforms therefore show a credible scalar loss while the + previously accepted guarded backshift remains substantially faster than + upstream ThinVec. +- Hypothesis: the accepted two-phase loop repeatedly derives the allocation data + pointer and maintains processed/deleted state after the first rejection. A + single read/write-cursor loop with one cached data pointer may reduce scalar + predicate-loop overhead while retaining one move per post-hole survivor and + identical unwind repair. +- Workloads: run the existing `retain_mixed` group without changing benchmark + code, preserving both `u64` and 64-byte JackVec measurements plus unchanged Vec + controls. Use the exact baseline and candidate commits in the paired runner. +- Hosts: exactly seven alternating rounds on the canonical Ryzen 7950X3D Linux + host pinned to CPU 0 with explicit system allocator, and exactly seven rounds + on the dedicated M3 Pro macOS host with explicit system allocator. Seed both + runs with `20260712`; use 100 samples, 3-second warm-up, 5-second measurement, + 100,000 resamples, and the strict host-idle gate. Never bypass host noise. +- Primary gate: `retain_mixed/u64/JackVec` must improve at least 5% at the paired + median on both hosts with each complete paired bootstrap interval below zero. + Continue mechanism-driven iteration toward Vec parity, but do not tune gates or + select rounds after observing results. +- Secondary gate: `retain_mixed/64_byte/JackVec` may not regress beyond 1% on + either host. Unchanged Vec controls outside the calibrated envelope require + explanation. Preserve the accepted large-element advantage over ThinVec. +- Safety gates: stable order and mutation semantics; exact-once owning drops; + empty, all-kept, all-rejected, ZST, over-aligned, predicate-panic, and rejected- + destructor-panic behavior; stable/MSRV/nightly/no-std/features/Clippy/docs; and + strict-provenance Tree Borrows Miri. +- Codegen/size gate: confirm the cached pointer and cursor state survive + optimization, retain one relocation per post-hole survivor, and reject + unexplained whole-text growth above 512 bytes. Record platform-specific codegen + differences rather than pooling them. +- Decision rule: accept only if every primary, secondary, safety, and size gate + passes. Otherwise revert the implementation and retain the complete negative + result. Any successor must state a new mechanism before measurement. + ### Preallocated four-element append (`perf/append-small-audit`) - Status: rejected; implementation and temporary diagnostic reverted From d466740e281e86f7188847457c5660622add4673 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 12:48:32 -0400 Subject: [PATCH 02/12] Scan retained elements with cached cursors --- src/lib.rs | 126 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2029795..1831e8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1045,82 +1045,108 @@ impl JackVec { if original_len == 0 { return; } + // Cache the data pointer as a raw pointer. This removes the per-element + // `self.data_raw()` indirection (one load + one LEA per element). + // `retain_mut` does not perform any operation that can reallocate, and + // safe predicate code receives only the current element, so this pointer + // remains valid for the loop. + let data: *mut T = self.data_raw(); unsafe { - // Prevent double-drop if a predicate or destructor panics after a - // hole has been created. The guard restores the initialized prefix. + // Prevent double-drop if a predicate or destructor panics. + // The guard restores the correct initialized prefix on drop. self.set_len_non_singleton(0); } - struct BackshiftOnDrop<'a, T> { + struct PanicGuard<'a, T> { vec: &'a mut JackVec, - processed_len: usize, - deleted_count: usize, + data: *mut T, + /// Next element to process (read cursor). + read: usize, + /// Next slot for a kept element (write cursor). + write: usize, + /// Length at the start of the call; bounds the unprocessed suffix. original_len: usize, } - impl Drop for BackshiftOnDrop<'_, T> { + impl Drop for PanicGuard<'_, T> { fn drop(&mut self) { unsafe { - if self.deleted_count > 0 { - let data = self.vec.data_raw(); + let deleted_count = self.read - self.write; + if deleted_count > 0 { + // Shift unprocessed elements [read..original_len] + // forward to overwrite the gap left by dropped + // elements. Matches the original + // `BackshiftOnDrop::drop` behavior: the post-panic + // vector state retains all non-rejected elements + // (kept + unprocessed) so the caller can observe + // and drop them. The slots [write..read) are + // "logically uninitialized" (moved from via + // ptr::read) and are not separately dropped. ptr::copy( - data.add(self.processed_len), - data.add(self.processed_len - self.deleted_count), - self.original_len - self.processed_len, + self.data.add(self.read), + self.data.add(self.write), + self.original_len - self.read, ); } + // Set final length. On success `read == original_len`, the + // copy count is zero, and `original_len - deleted_count` + // equals `write`, the number of retained elements. On panic, + // it's + // `original_len - deleted_count` (the kept count plus + // the unprocessed suffix). self.vec - .set_len_non_singleton(self.original_len - self.deleted_count); + .set_len_non_singleton(self.original_len - deleted_count); } } } - fn process_loop( - original_len: usize, - f: &mut F, - guard: &mut BackshiftOnDrop<'_, T>, - ) where - F: FnMut(&mut T) -> bool, - { - while guard.processed_len != original_len { - let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; - if !f(current) { - // Advance before dropping so the guard will not touch this - // element again if its destructor panics. - guard.processed_len += 1; - guard.deleted_count += 1; - unsafe { - ptr::drop_in_place(current); - } - if DELETED { - continue; - } - break; - } + let mut guard = PanicGuard { + vec: self, + data, + read: 0, + write: 0, + original_len, + }; - if DELETED { + // Single-pass scan with read/write cursors. For each element, if kept, + // move it to the write position (no-op if write == read); if dropped, + // call its destructor. Avoids the per-iter data_raw() indirection + // and the deleted_count subtraction of the two-phase design. + while guard.read < guard.original_len { + let current = unsafe { &mut *guard.data.add(guard.read) }; + if f(current) { + if guard.write < guard.read { + // Ownership transfer via memcpy: ptr::read leaves the + // source slot logically uninitialized (no destructor + // call), ptr::write constructs the value at the write + // position. The source slot is not accessed again. unsafe { - let hole = guard - .vec - .data_raw() - .add(guard.processed_len - guard.deleted_count); - ptr::copy_nonoverlapping(current, hole, 1); + ptr::write( + guard.data.add(guard.write), + ptr::read(guard.data.add(guard.read)), + ); } } - guard.processed_len += 1; + guard.write += 1; + } else { + // Advance read BEFORE drop so that the PanicGuard's drop + // sees the correct unprocessed count, and so that the + // drop's ptr::copy shift excludes the element we just + // processed (matching the original code's "Advance before + // dropping" comment). + guard.read += 1; + unsafe { + ptr::drop_in_place(current); + } + continue; } + guard.read += 1; } - let mut guard = BackshiftOnDrop { - vec: self, - processed_len: 0, - deleted_count: 0, - original_len, - }; - process_loop::(original_len, &mut f, &mut guard); - process_loop::(original_len, &mut f, &mut guard); - drop(guard); + // Successful completion: let the PanicGuard drop run. Since + // `read == original_len`, its copy is a zero-length no-op and it + // publishes `write` as the retained length. } /// Removes consecutive elements in the vector that resolve to the same key. From 10c87f6b9e8adf564bf0098c8215d1d95fc9af31 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 12:59:23 -0400 Subject: [PATCH 03/12] Revert "Scan retained elements with cached cursors" This reverts commit d466740e281e86f7188847457c5660622add4673. --- src/lib.rs | 126 +++++++++++++++++++++-------------------------------- 1 file changed, 50 insertions(+), 76 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1831e8d..2029795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1045,108 +1045,82 @@ impl JackVec { if original_len == 0 { return; } - // Cache the data pointer as a raw pointer. This removes the per-element - // `self.data_raw()` indirection (one load + one LEA per element). - // `retain_mut` does not perform any operation that can reallocate, and - // safe predicate code receives only the current element, so this pointer - // remains valid for the loop. - let data: *mut T = self.data_raw(); unsafe { - // Prevent double-drop if a predicate or destructor panics. - // The guard restores the correct initialized prefix on drop. + // Prevent double-drop if a predicate or destructor panics after a + // hole has been created. The guard restores the initialized prefix. self.set_len_non_singleton(0); } - struct PanicGuard<'a, T> { + struct BackshiftOnDrop<'a, T> { vec: &'a mut JackVec, - data: *mut T, - /// Next element to process (read cursor). - read: usize, - /// Next slot for a kept element (write cursor). - write: usize, - /// Length at the start of the call; bounds the unprocessed suffix. + processed_len: usize, + deleted_count: usize, original_len: usize, } - impl Drop for PanicGuard<'_, T> { + impl Drop for BackshiftOnDrop<'_, T> { fn drop(&mut self) { unsafe { - let deleted_count = self.read - self.write; - if deleted_count > 0 { - // Shift unprocessed elements [read..original_len] - // forward to overwrite the gap left by dropped - // elements. Matches the original - // `BackshiftOnDrop::drop` behavior: the post-panic - // vector state retains all non-rejected elements - // (kept + unprocessed) so the caller can observe - // and drop them. The slots [write..read) are - // "logically uninitialized" (moved from via - // ptr::read) and are not separately dropped. + if self.deleted_count > 0 { + let data = self.vec.data_raw(); ptr::copy( - self.data.add(self.read), - self.data.add(self.write), - self.original_len - self.read, + data.add(self.processed_len), + data.add(self.processed_len - self.deleted_count), + self.original_len - self.processed_len, ); } - // Set final length. On success `read == original_len`, the - // copy count is zero, and `original_len - deleted_count` - // equals `write`, the number of retained elements. On panic, - // it's - // `original_len - deleted_count` (the kept count plus - // the unprocessed suffix). self.vec - .set_len_non_singleton(self.original_len - deleted_count); + .set_len_non_singleton(self.original_len - self.deleted_count); } } } - let mut guard = PanicGuard { - vec: self, - data, - read: 0, - write: 0, - original_len, - }; - - // Single-pass scan with read/write cursors. For each element, if kept, - // move it to the write position (no-op if write == read); if dropped, - // call its destructor. Avoids the per-iter data_raw() indirection - // and the deleted_count subtraction of the two-phase design. - while guard.read < guard.original_len { - let current = unsafe { &mut *guard.data.add(guard.read) }; - if f(current) { - if guard.write < guard.read { - // Ownership transfer via memcpy: ptr::read leaves the - // source slot logically uninitialized (no destructor - // call), ptr::write constructs the value at the write - // position. The source slot is not accessed again. + fn process_loop( + original_len: usize, + f: &mut F, + guard: &mut BackshiftOnDrop<'_, T>, + ) where + F: FnMut(&mut T) -> bool, + { + while guard.processed_len != original_len { + let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; + if !f(current) { + // Advance before dropping so the guard will not touch this + // element again if its destructor panics. + guard.processed_len += 1; + guard.deleted_count += 1; unsafe { - ptr::write( - guard.data.add(guard.write), - ptr::read(guard.data.add(guard.read)), - ); + ptr::drop_in_place(current); + } + if DELETED { + continue; } + break; } - guard.write += 1; - } else { - // Advance read BEFORE drop so that the PanicGuard's drop - // sees the correct unprocessed count, and so that the - // drop's ptr::copy shift excludes the element we just - // processed (matching the original code's "Advance before - // dropping" comment). - guard.read += 1; - unsafe { - ptr::drop_in_place(current); + + if DELETED { + unsafe { + let hole = guard + .vec + .data_raw() + .add(guard.processed_len - guard.deleted_count); + ptr::copy_nonoverlapping(current, hole, 1); + } } - continue; + guard.processed_len += 1; } - guard.read += 1; } - // Successful completion: let the PanicGuard drop run. Since - // `read == original_len`, its copy is a zero-length no-op and it - // publishes `write` as the retained length. + let mut guard = BackshiftOnDrop { + vec: self, + processed_len: 0, + deleted_count: 0, + original_len, + }; + process_loop::(original_len, &mut f, &mut guard); + process_loop::(original_len, &mut f, &mut guard); + drop(guard); } /// Removes consecutive elements in the vector that resolve to the same key. From 03bd5984abd47a12d4ae0af65342edad590c15b7 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 12:59:45 -0400 Subject: [PATCH 04/12] Cache retain allocation pointer --- src/lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2029795..15aa22a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1045,6 +1045,7 @@ impl JackVec { if original_len == 0 { return; } + let data = self.data_raw(); unsafe { // Prevent double-drop if a predicate or destructor panics after a @@ -1054,6 +1055,7 @@ impl JackVec { struct BackshiftOnDrop<'a, T> { vec: &'a mut JackVec, + data: *mut T, processed_len: usize, deleted_count: usize, original_len: usize, @@ -1063,10 +1065,9 @@ impl JackVec { fn drop(&mut self) { unsafe { if self.deleted_count > 0 { - let data = self.vec.data_raw(); ptr::copy( - data.add(self.processed_len), - data.add(self.processed_len - self.deleted_count), + self.data.add(self.processed_len), + self.data.add(self.processed_len - self.deleted_count), self.original_len - self.processed_len, ); } @@ -1084,7 +1085,7 @@ impl JackVec { F: FnMut(&mut T) -> bool, { while guard.processed_len != original_len { - let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; + let current = unsafe { &mut *guard.data.add(guard.processed_len) }; if !f(current) { // Advance before dropping so the guard will not touch this // element again if its destructor panics. @@ -1101,10 +1102,7 @@ impl JackVec { if DELETED { unsafe { - let hole = guard - .vec - .data_raw() - .add(guard.processed_len - guard.deleted_count); + let hole = guard.data.add(guard.processed_len - guard.deleted_count); ptr::copy_nonoverlapping(current, hole, 1); } } @@ -1114,6 +1112,7 @@ impl JackVec { let mut guard = BackshiftOnDrop { vec: self, + data, processed_len: 0, deleted_count: 0, original_len, From 20ccd48edb9ad7e7c4054c5d00879967edb2bc1f Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:19:32 -0400 Subject: [PATCH 05/12] Revert "Cache retain allocation pointer" This reverts commit 03bd5984abd47a12d4ae0af65342edad590c15b7. --- src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 15aa22a..2029795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1045,7 +1045,6 @@ impl JackVec { if original_len == 0 { return; } - let data = self.data_raw(); unsafe { // Prevent double-drop if a predicate or destructor panics after a @@ -1055,7 +1054,6 @@ impl JackVec { struct BackshiftOnDrop<'a, T> { vec: &'a mut JackVec, - data: *mut T, processed_len: usize, deleted_count: usize, original_len: usize, @@ -1065,9 +1063,10 @@ impl JackVec { fn drop(&mut self) { unsafe { if self.deleted_count > 0 { + let data = self.vec.data_raw(); ptr::copy( - self.data.add(self.processed_len), - self.data.add(self.processed_len - self.deleted_count), + data.add(self.processed_len), + data.add(self.processed_len - self.deleted_count), self.original_len - self.processed_len, ); } @@ -1085,7 +1084,7 @@ impl JackVec { F: FnMut(&mut T) -> bool, { while guard.processed_len != original_len { - let current = unsafe { &mut *guard.data.add(guard.processed_len) }; + let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; if !f(current) { // Advance before dropping so the guard will not touch this // element again if its destructor panics. @@ -1102,7 +1101,10 @@ impl JackVec { if DELETED { unsafe { - let hole = guard.data.add(guard.processed_len - guard.deleted_count); + let hole = guard + .vec + .data_raw() + .add(guard.processed_len - guard.deleted_count); ptr::copy_nonoverlapping(current, hole, 1); } } @@ -1112,7 +1114,6 @@ impl JackVec { let mut guard = BackshiftOnDrop { vec: self, - data, processed_len: 0, deleted_count: 0, original_len, From cb5b22189d60159703fd51c32f8d6df6619d92f1 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:20:40 -0400 Subject: [PATCH 06/12] Record retain cursor results and next mechanism --- TODO.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/TODO.md b/TODO.md index c212e8c..7ace01b 100644 --- a/TODO.md +++ b/TODO.md @@ -249,6 +249,31 @@ header alignment without a new real-workload counterexample. - Decision rule: accept only if every primary, secondary, safety, and size gate passes. Otherwise revert the implementation and retain the complete negative result. Any successor must state a new mechanism before measurement. +- Candidate 1 (`d466740`, reverted by `10c87f6`): rejected. Combining pointer + caching with a single branchy read/write loop regressed Linux `u64` by 19.21% + (interval +18.11%..+20.19%) and 64-byte elements by 6.61% + (+3.25%..+8.16%), with neutral Vec controls. On the M3 Pro it regressed `u64` + by 9.15% (+6.42%..+10.66%) and was neutral for 64-byte elements, again with + neutral controls. The existing const-specialized pre-hole/post-hole loops are + essential; do not unify them. +- Focused-baseline finding: in the controlled retain-only binary, baseline + JackVec `u64` is already at parity with or faster than Vec on both hosts. The + larger five-library ratio is not a stable scalar implementation gap. The + reproducible focused gap is the 64-byte M3 Pro case (roughly 11%). +- Successor 1 (`03bd598`, reverted by `20ccd48`): rejected. Preserving both loops + while caching only the data pointer was neutral on the M3 Pro (`u64` +0.54%, + 64-byte -0.15%, both intervals spanning zero), and the baseline/candidate + Mach-O benchmark executables were byte-identical. Linux was also inconclusive + while unchanged Vec controls moved materially, so it supplies no acceptance + evidence. LLVM already eliminates the proposed pointer work. +- Successor 2 mechanism, pre-registered before implementation: for a 64-byte, + 8-aligned element whose source and destination are both 8 modulo 16, test an + 8-byte edge / 48-byte 16-aligned body / 8-byte edge relocation. The ordinary + `copy_nonoverlapping` path remains the fallback. This targets the M3 Pro's + compact-header alignment without changing allocation layout. Accept only if + the 64-byte workload improves at least 5% on the M3 Pro with its interval below + zero, Linux 64-byte and both `u64` workloads stay within 1%, strict Miri passes, + and local/whole-text growth stays within the existing size gate. ### Preallocated four-element append (`perf/append-small-audit`) From 7a7f78e1127a15b111cecdc24393a22a353aae65 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:21:14 -0400 Subject: [PATCH 07/12] Align middle of offset retain relocations --- src/lib.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2029795..7147a25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1083,6 +1083,27 @@ impl JackVec { ) where F: FnMut(&mut T) -> bool, { + #[inline(always)] + unsafe fn copy_retained(source: *mut T, destination: *mut T) { + if mem::size_of::() == 64 + && mem::align_of::() == 8 + && source.addr() & 15 == 8 + && destination.addr() & 15 == 8 + { + let source = source.cast::(); + let destination = destination.cast::(); + ptr::copy_nonoverlapping(source, destination, 8); + ptr::copy_nonoverlapping( + source.add(8).cast::(), + destination.add(8).cast::(), + 3, + ); + ptr::copy_nonoverlapping(source.add(56), destination.add(56), 8); + } else { + ptr::copy_nonoverlapping(source, destination, 1); + } + } + while guard.processed_len != original_len { let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; if !f(current) { @@ -1105,7 +1126,7 @@ impl JackVec { .vec .data_raw() .add(guard.processed_len - guard.deleted_count); - ptr::copy_nonoverlapping(current, hole, 1); + copy_retained(current, hole); } } guard.processed_len += 1; From 326a79ba551b36f5f2e0a3b20e7a53fdad8bd3e2 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:30:26 -0400 Subject: [PATCH 08/12] Revert "Align middle of offset retain relocations" This reverts commit 7a7f78e1127a15b111cecdc24393a22a353aae65. --- src/lib.rs | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7147a25..2029795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1083,27 +1083,6 @@ impl JackVec { ) where F: FnMut(&mut T) -> bool, { - #[inline(always)] - unsafe fn copy_retained(source: *mut T, destination: *mut T) { - if mem::size_of::() == 64 - && mem::align_of::() == 8 - && source.addr() & 15 == 8 - && destination.addr() & 15 == 8 - { - let source = source.cast::(); - let destination = destination.cast::(); - ptr::copy_nonoverlapping(source, destination, 8); - ptr::copy_nonoverlapping( - source.add(8).cast::(), - destination.add(8).cast::(), - 3, - ); - ptr::copy_nonoverlapping(source.add(56), destination.add(56), 8); - } else { - ptr::copy_nonoverlapping(source, destination, 1); - } - } - while guard.processed_len != original_len { let current = unsafe { &mut *guard.vec.data_raw().add(guard.processed_len) }; if !f(current) { @@ -1126,7 +1105,7 @@ impl JackVec { .vec .data_raw() .add(guard.processed_len - guard.deleted_count); - copy_retained(current, hole); + ptr::copy_nonoverlapping(current, hole, 1); } } guard.processed_len += 1; From 1edc4fc788c4028395a0a61a7a0cc7e9c293675d Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:31:06 -0400 Subject: [PATCH 09/12] Record aligned retain copy result and layout hypothesis --- TODO.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/TODO.md b/TODO.md index 7ace01b..cb6ed93 100644 --- a/TODO.md +++ b/TODO.md @@ -274,6 +274,24 @@ header alignment without a new real-workload counterexample. the 64-byte workload improves at least 5% on the M3 Pro with its interval below zero, Linux 64-byte and both `u64` workloads stay within 1%, strict Miri passes, and local/whole-text growth stays within the existing size gate. +- Successor 2 result (`7a7f78e`, reverted by `326a79b`): rejected. The M3 Pro + 64-byte workload improved 3.13% with an interval of -4.30%..-1.62%, but missed + the fixed 5% threshold. Linux 64-byte was inconclusive and Linux `u64` + regressed 8.95% despite the specialized branch being compile-time unreachable; + an unchanged Linux Vec control also moved -3.05%. Keeping the specialization + would select a sub-threshold platform point while ignoring layout-sensitive + regressions. +- Successor 3 mechanism, pre-registered before implementation: align the data + region to 16 bytes when `T` is at least 64 bytes with natural alignment 8. + This adds exactly 8 requested bytes per non-empty allocation while preserving + the one-word owner, 8-byte header, contiguous slice, and reconstructable layout. + It should remove the compact-header 8-mod-16 offset for every operation on such + large elements rather than specializing retain's copy loop. Primary gate: M3 + Pro 64-byte retain improves at least 5% with the interval below zero. Linux + 64-byte and both `u64` retain workloads may not regress beyond 1%; record the + requested/usable allocation delta and require no allocation-count change. + Acceptance additionally requires targeted dedup and traversal evidence because + this is a representation-layout change, plus the complete safety/size gates. ### Preallocated four-element append (`perf/append-small-audit`) From 64890d3ee5c76959beed39bc7f41ff9b3e43147e Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:31:58 -0400 Subject: [PATCH 10/12] Align large element allocations to sixteen bytes --- src/lib.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2029795..fa17a13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,7 +218,12 @@ fn padding() -> usize { /// Gets the align necessary to allocate a `JackVec` fn alloc_align() -> usize { - max(mem::align_of::(), mem::align_of::
()) + let natural = max(mem::align_of::(), mem::align_of::
()); + if mem::size_of::() >= 64 && mem::align_of::() == 8 { + max(natural, 16) + } else { + natural + } } fn growth_capacity(old_cap: usize, min_cap: usize) -> usize { @@ -2895,6 +2900,11 @@ mod tests { let v = JackVec::::new(); assert!(v.data_raw() as usize % core::mem::align_of::() == 0); + + type Large = [u64; 8]; + let v = JackVec::::with_capacity(1); + assert_eq!(v.data_raw().addr() % 16, 0); + assert_eq!(super::alloc_size::(1), 80); } #[test] From 5e6bab5e0b5be5bea9fc913a43d70fd5e529e4b2 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:40:59 -0400 Subject: [PATCH 11/12] Revert "Align large element allocations to sixteen bytes" This reverts commit 64890d3ee5c76959beed39bc7f41ff9b3e43147e. --- src/lib.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fa17a13..2029795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,12 +218,7 @@ fn padding() -> usize { /// Gets the align necessary to allocate a `JackVec` fn alloc_align() -> usize { - let natural = max(mem::align_of::(), mem::align_of::
()); - if mem::size_of::() >= 64 && mem::align_of::() == 8 { - max(natural, 16) - } else { - natural - } + max(mem::align_of::(), mem::align_of::
()) } fn growth_capacity(old_cap: usize, min_cap: usize) -> usize { @@ -2900,11 +2895,6 @@ mod tests { let v = JackVec::::new(); assert!(v.data_raw() as usize % core::mem::align_of::() == 0); - - type Large = [u64; 8]; - let v = JackVec::::with_capacity(1); - assert_eq!(v.data_raw().addr() % 16, 0); - assert_eq!(super::alloc_size::(1), 80); } #[test] From 20b5e3e4d0a1ed29af410f066667e7371b9f9206 Mon Sep 17 00:00:00 2001 From: Thomas Santerre Date: Sun, 12 Jul 2026 13:42:21 -0400 Subject: [PATCH 12/12] Close retain cursor audit --- TODO.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index cb6ed93..2b58958 100644 --- a/TODO.md +++ b/TODO.md @@ -210,7 +210,7 @@ header alignment without a new real-workload counterexample. ### Retain cursor and publication audit (`perf/retain-cursor-audit`) -- Status: in progress; pre-registered before committing the candidate +- Status: complete; all candidates rejected and implementation restored - Baseline: this ledger-only commit, whose `src/lib.rs` is identical to canonical merge commit `323ae2f`. - Observation: the canonical Linux matrix measures `retain_mixed/u64` at 1.234x @@ -292,6 +292,25 @@ header alignment without a new real-workload counterexample. requested/usable allocation delta and require no allocation-count change. Acceptance additionally requires targeted dedup and traversal evidence because this is a representation-layout change, plus the complete safety/size gates. +- Successor 3 result (`64890d3`, reverted by `5e6bab5`): rejected. Adding eight + requested bytes aligned the large-element data region but regressed Linux + 64-byte retain 9.77% with interval +6.99%..+10.63%. The M3 Pro result was an + inconclusive -1.40%; all scalar and Vec controls were neutral. Data alignment + is not the dominant retain cost, and the memory trade cannot be justified. +- Final codegen audit: isolated no-inline AArch64 JackVec and Vec wrappers emit + the same predicate test and the same four 128-bit load/store instructions for + each retained 64-byte element. JackVec's remaining differences load and publish + its allocation-header length outside the per-element hot loop. The pointer-only + candidate produces a byte-identical M3 Pro benchmark executable, proving LLVM + already removes that proposed work. +- Final decision: retain the canonical guarded two-phase implementation. It is + already at focused Vec parity or better for `u64` on both hosts and within about + 3% for Linux 64-byte elements. The remaining M3 Pro 64-byte difference is not + explained by removable per-element instructions, pointer derivation, copy + alignment, or allocation alignment. The five-library scalar ratio is + code-layout-sensitive and must not motivate specialization. Preserve all raw + artifacts under `retain-{cursor,pointer,aligned-copy,large-align}-*` on their + respective hosts and do not reopen retain without a new workload or mechanism. ### Preallocated four-element append (`perf/append-small-audit`)