Skip to content

perf(sdk): O(1) size-class guest allocator replacing first-fit linked list - #1746

Draft
0xAndoroid wants to merge 2 commits into
mainfrom
perf/size-class-guest-allocator
Draft

perf(sdk): O(1) size-class guest allocator replacing first-fit linked list#1746
0xAndoroid wants to merge 2 commits into
mainfrom
perf/size-class-guest-allocator

Conversation

@0xAndoroid

Copy link
Copy Markdown
Collaborator

Problem

The no_std guest allocator (linked_list_allocator first-fit, selected via ZeroOS's alloc-linked-list feature) walks its free list on every alloc and dealloc. 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:

58.62%  <linked_list_allocator::hole::HoleList>::allocate_first_fit
27.10%  <linked_list_allocator::hole::HoleList>::deallocate
 5.57%  <k256::arithmetic::projective::ProjectivePoint>::add   (actual workload…)

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_alloc

  • Allocations round up to a power-of-two size class (min 8 B); each class keeps a singly-linked free list (freed blocks store next in word 0). Alloc pops the head or bumps the arena cursor; dealloc pushes. Every operation is a few dozen instructions; nothing searches.
  • Bump blocks are class-size aligned, so any align ≤ size is 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.
  • realloc grows the newest bump block in place (catches Vec doubling), and treats same-class or shrinking requests as pointer-stable no-ops.
  • No splitting/coalescing: bounded internal fragmentation (<2× per live block) + per-class high-water free lists, deliberately traded for O(1) — appropriate for single-run guest heaps.
  • Registered in __platform_bootstrap before kinit, for target_os = "none" guests only. std/musl guests keep the ZeroOS allocator (musl's malloc manages its own arenas over mmap/brk).

Testing

  • Host-runnable unit tests (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-guest builds 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.

… 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
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant