fix(page): derive the extend bound from the geometry, not a 64 KiB literal - #34
Merged
Conversation
…teral `page_extend` bounds its batch at 4 KiB of payload and computed it as `span_shift = 4 + slice_count.trailing_zeros()`. The identity behind that is `4096/bsize == reserved / (slice_count * 16)`, and the `16` is `SEGMENT_SLICE_SIZE / 4096` -- so the literal `4` is correct only at the shipped 64 KiB slice. Under `--cfg ra_small_profile` the slice is 4 KiB, so the bound was 256 bytes of payload instead of 4 KiB. For a 512-byte class the batch computes to 0, `.max(1)` clamps it to ONE, and every page carried `capacity == 1` -- no second block for the fast path to find, so `malloc_generic` ran on 100% of allocations. Counted, 100,000 alloc+free pairs at the small profile, generic entries per op: 512 B 1.0000 -> 0.1250, 513 B 1.0000 -> 0.1667, 1 KiB 1.0000 -> 0.2500; page carve-and-retire churn 195 per 100,000 -> 24/32/49. On a 32-bit host the 512-vs-513 step inverts from +8.6% (slower) to -36% (faster), 512 absolute 390,200 -> ~200,000 ns for the same work, reproduced three times. The default geometry does not move: 65536.trailing_zeros() - 12 == 4, the old literal, and the all-features x86-64 asm diff against main changes one symbol, the debug-record blob, with no executable function touched. Found by tracing collect_inner's reclaim while chasing the Kairos RTOS's 512-byte step: every reclaimed page printed cap=1 against resv=8, and reserved being right while capacity was 1 named the extend. Gates: 20 suites at default / small / 256k, gate-selftest 11/11, wasm 20,168, census clean, clippy 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Withdraws the "not measured on silicon" caveat. XIAO ESP32-S3, main against the fix, one board and one session, same 192 KiB region, same 166 ns/op floor and identical checksums on every row (work parity): pingpong 32 B 595 -> 518 13.0% faster batch 64 mixed 8-512 833 -> 702 15.7% faster churn 64 live 8-512 1,011 -> 856 15.3% faster large 2,048 B 1,134 -> 1,121 1.1% 13-16% on every binned workload, the same magnitude as the 16% step the report opened with. 2,048 B barely moving is the tell that it is the same mechanism: that size is on the bin route, which still enters malloc_generic on every op and is untouched here. Co-Authored-By: Claude Opus 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.
Follows the Kairos RTOS step report (
docs/plans/finished/fixed-prim-small-step.md, §8.6–8.7). PR #32 corrected the attribution; this fixes the defect that chase led to.The defect
page_extendbounds its batch at 4 KiB of payload and computed it as:The identity is
4096 / bsize == reserved / (slice_count * 16), and the16isSEGMENT_SLICE_SIZE / 4096. So the literal4islog2(16)and holds only at the shipped 64 KiB slice.Under
--cfg ra_small_profilethe slice is 4 KiB, the true factor is 1, and the bound was 256 bytes of payload instead of 4 KiB — sixteen times too small. For a 512-byte class8 >> 4 == 0,.max(1)rescues it to one, and every page was extended one block at a time. A page withcapacity == 1has no second block, so the fast path could never hit andmalloc_genericran on 100% of allocations.Measured
Counters, 100,000 alloc+free pairs, small profile — deterministic, no clock:
generic/op beforePage carve-and-retire churn: 195 per 100,000 → 24 / 32 / 49.
Timing, 32-bit host, ABBA-interleaved, reproduced three times: the 512-vs-513 step inverts from +8.6% (slower) to −36% (faster), and 512 in absolute terms goes 390,200 ns → ~200,000 ns for the same 50,000 pairs, about 1.9× faster.
Blast radius
The default geometry does not move.
65536.trailing_zeros() - 12 == 4, the old literal. The all-features x86-64 assembly diff againstmainchanges exactly one symbol — the debug-record blob — with no executable function touched. This is ara_small_profilefix.Not claimed
Not measured on silicon; that rig is the consumer's. The prediction to falsify is §7's
heap_4A/B: 256–512 should stop being the only rangerusty_allocloses. Also still open, and untouched by this: the bin route entersmalloc_genericon every op even after the fix.Gates
20 suites at default / small profile / 256k geometry, gate-selftest 11/11, wasm 20,168 (baseline 20,169), unsafe census clean, clippy 0.
🤖 Generated with Claude Code