Repository navigation
fix(heap): stranded large spans after adoption; perf: segment scrub, large cache (2.2.4) - #47
Merged
Merged
Conversation
…large cache Fixes (docs/plans/huge-free-retention.md, a consumer report): - adopt_segment retired ONE dead large span per adopted segment and left the rest carved forever (a large span is never queued, so no collect reaches it). A model loaded on a thread that then exited, and dropped on another, kept ~0.6 of its size resident whatever purge_delay said. Now every dead span is retired; the walk resumes at the merged span. Adoption also no longer re-counts and releases the empty segment that retire_span had just parked. - huge_free purges a dead huge block (> 32 MiB) before its chunks rejoin the arena when purging is on. It stayed resident: the consumer's unexplained ~150 MB was Whisper small's 152 MiB embedding. Performance (callgrind; memset counted as real instructions): - scrub_recycled: a recycled chunk zeroes the owner table and the slots read before a carve, not the whole 45 KB header; slots are zeroed at their first carve. Huge segments skip 511 strided slot writes and wait_no_remote_in_flight scans slot 1 only. 64 MiB malloc+free 8,959 -> 824 Ir; thread life -12.4 %; Rust thread-pool workload -15.0 %. - Per-heap large-span cache (<= 4 MiB, purging off, lone frees only, so a teardown run never leaves a span to be refilled out of order): 2 MiB malloc/free 293 -> 138 Ir. - Multi-chunk arena claim: one bitmap load per word, a one-word run claimed with one masked fetch_or. find_arena replaces three copies of the scan. Every opscan op unchanged or better; perl/sqlite/Endless Sky allocator Ir flat. Refuted and recorded in docs/opps.md: segment_map RMW batching (+15), masked chunk release (+9), flush-on-miss in large_alloc (+3 on every generic op). Validated: workspace --all-features, linux-gates (42 suites), embedded matrix 15/15, miri gate, semgrep, mimalloc-bench sweep 19/19, churn, real-world programs byte-identical across ra/mi/sys, alloc-eval checks on 8 game traces, downstream corpus (spacedb x3, rusty_alloc_default, rusty_zstd). Unsafe census 973 -> 977 (UNSAFE.md); wasm baseline 20321 -> 20873 B gz (deliberate). corpus/linux-gates.sh no longer hard-codes a /mnt/c path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The heartbeat in Heap::malloc_generic_body called options::deferred_free while malloc_generic's `&mut self` was live and protected. A hook that allocates (the openheimer test's) or frees re-entered the same heap through a raw pointer under that borrow: undefined behaviour under Stacked Borrows. Miri has reported it on every main run since 2.2.3, which kept the CI badge red. The heartbeat now only marks the hook DUE (Heap::deferred_due, set when options::deferred_registered() peeks a hook). Every allocation entry in alloc.rs fires a due hook BEFORE forming &mut Heap (fire_deferred_if_due); malloc_slow keeps both arms in tail position, jumping to the cold malloc_slow_deferred when due. Upstream fires the hook at the start of the generic path; here it runs at the start of the next entry, within the hook's "called occasionally, with a heartbeat" contract. Cost, measured: big 91 -> 94, mixed 92.04 -> 94.26 Ir/op; perl/sqlite allocator Ir +0.16 % / +0.22 %. The first shape (a call inside malloc_slow) gave it a frame and cost big +9; refuted, recorded. Miri: tests/openheimer.rs now passes, 37/37 (it aborted at the first UB before, so the suites after it had not run under Miri in CI since 2.2.3). Unsafe census 977 -> 981 (UNSAFE.md). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intended release: 2.2.4.
Fixes (consumer report,
docs/plans/huge-free-retention.md)adopt_segmentretired one dead large span per adopted segment and left the rest carved for the life of the process. A model loaded on a thread that exited and dropped on another kept ~0.6 of its size resident, whateverpurge_delaysaid (probe: 1.62 -> 1.01 models). Adoption also stopped re-counting and releasing the empty segmentretire_spanhad just parked.Performance (callgrind, memset counted as real instructions; vs 2.2.3)
Every opscan op unchanged or better (big 92 -> 91, mixed 92.70 -> 92.04). perl/sqlite allocator Ir flat; Endless Sky start-up output identical, allocator Ir flat. wasm +361 B gz (baseline moved deliberately).
Validated locally: workspace
--all-features177/177,corpus/linux-gates.sh42 suites, embedded matrix 15/15, miri gate, semgrep, mimalloc-bench sweep 19/19, churn 5/5, real-world programs byte-identical across ra/mi/sys, alloc-eval checks on 8 game traces, downstream corpus (spacedb x3, rusty_alloc_default, rusty_zstd; rusty_maplibre is the known 2.0.0 break).Refutations recorded in
docs/opps.md. Unsafe census 973 -> 977, each site inUNSAFE.md.🤖 Generated with Claude Code