perf(sdk): O(1) size-class guest allocator replacing first-fit linked list - #1746
Draft
0xAndoroid wants to merge 2 commits into
Draft
perf(sdk): O(1) size-class guest allocator replacing first-fit linked list#17460xAndoroid wants to merge 2 commits into
0xAndoroid wants to merge 2 commits into
Conversation
… list Every executed RISC-V instruction is a proving-cost cycle, and the no_std guest allocator (linked_list_allocator via ZeroOS, first-fit) walks its free list on every alloc AND dealloc. On an allocation-heavy guest — full stateless Ethereum mainnet block validation (revm + MPT, ~1.5 GiB heap) — PC-sampling attributed 85.7% of ALL executed instructions to the allocator (58.6% allocate_first_fit + 27.1% deallocate); total trace length dropped ~6x (29.8B -> 4.9B cycles) after this swap. In-repo btreemap example: -8.9% rows at n=6000, -21% at n=20000 (the walk cost scales with live blocks). jolt_platform::size_class_alloc: power-of-two size-class free lists over a bump arena. Alloc pops the class head or bumps; dealloc pushes; realloc grows the newest bump block in place (Vec doubling) and treats same-class/shrinking requests as no-ops. No searching, splitting, or coalescing - bounded internal fragmentation (<2x per live block) traded for O(1) everything. Single-hart by design, like the rest of the guest runtime. Registered in __platform_bootstrap for target_os = "none" guests only (std/musl guests keep ZeroOS's allocator; musl malloc manages its own arenas). Host-runnable unit tests cover class rounding, alignment-guarded reuse, in-place growth, copy-growth, shrink, exhaustion, and churn recycling.
0xAndoroid
marked this pull request as draft
August 6, 2026 21:49
malloc_shim::realloc unconditionally did malloc + copy + free, so C growth loops never reached the resize fast paths of the underlying allocator. With size_class_alloc that means missing both the same-class no-op and the in-place bump growth — the paths its doc comment calls out as what "Vec doubling hits constantly" — and unlike the linked-list allocator it replaces, size_class_alloc does not coalesce, so every superseded block is stranded in its class for the rest of the run. Route through alloc::alloc::realloc instead and rewrite the header at the returned block. The allocator carries the header along with the payload when it has to move, and returns the block unchanged when it can resize in place. The shim stays allocator-agnostic. Measured with a C growth chain (malloc(16) doubling to 4 KiB, contents verified after every resize) on riscv64imac guest, execute-only rows: 8 chains: 1,052,881 -> 1,030,041 (-22,840) 16 chains: 2,104,857 -> 2,059,249 (-45,608) Marginal cost per chain 131,497 -> 128,651 rows, i.e. ~356 rows saved per realloc call. Guest output is byte-identical across the ZeroOS linked-list allocator, size_class_alloc with the old shim, and size_class_alloc with this shim. 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.
Problem
The no_std guest allocator (
linked_list_allocatorfirst-fit, selected via ZeroOS'salloc-linked-listfeature) walks its free list on everyallocanddealloc. In a zkVM every executed instruction is a proving-cost cycle, so this walk is pure trace-length overhead — and it grows with the number of live blocks.Measured on a real workload (full stateless Ethereum mainnet block validation — reth/revm + MPT witness reveal over a 1.5 GiB heap, jeth): PC-sampled instruction profile attributed 85.7% of all executed instructions to the allocator:
Swapping in this allocator cut total trace length ~6× (29.76B → 4.94B cycles for a 58M-gas block; 10.79B → 2.30B for a 31.8M-gas block), with identical guest outputs.
In-repo evidence (
examples/btreemap, execute-only row counts): −8.9% rows at n=6000, −21% at n=20000 — the win scales with live-block count exactly as expected from the O(free-list) walk.Design —
jolt_platform::size_class_allocnextin word 0). Alloc pops the head or bumps the arena cursor; dealloc pushes. Every operation is a few dozen instructions; nothing searches.align ≤ sizeis satisfied; larger aligns round the class up. Free-list reuse re-checks the requested alignment (in-place-grown blocks only guarantee their original class alignment) and falls through to the bump path on mismatch.reallocgrows the newest bump block in place (catches Vec doubling), and treats same-class or shrinking requests as pointer-stable no-ops.__platform_bootstrapbeforekinit, fortarget_os = "none"guests only. std/musl guests keep the ZeroOS allocator (musl's malloc manages its own arenas over mmap/brk).Testing
cargo nextest run -p jolt-platform -E 'test(size_class)'): class rounding, alignment-guarded reuse, LIFO recycling, in-place growth, copy-growth preserving contents, shrink stability, oversize/exhaustion nulls, alloc/free churn.btreemap-guestbuilds and executes to completion with matching (non-panicking) results on the emulator; the jeth workload additionally self-checks via its post-state-root assertion (any allocator bug ⇒ wrong state root ⇒ panic).Note: GitHub Actions may be slow to schedule (ongoing incident) — happy to rebase/retrigger as needed.