Skip to content

perf: make the small-object allocation fast path a leaf - #15011

Draft
Kha wants to merge 1 commit into
masterfrom
alloc-leaf-fastpath
Draft

perf: make the small-object allocation fast path a leaf#15011
Kha wants to merge 1 commit into
masterfrom
alloc-leaf-fastpath

Conversation

@Kha

@Kha Kha commented Sep 3, 2026

Copy link
Copy Markdown
Member

This PR shortens lean_alloc_small_object_core from 18 to 14 instructions by keeping mimalloc's generic allocation path out of line, so that the fast path needs no stack frame.

The entry point now tests page->free itself and tail-jumps to a cold helper on a miss, instead of calling mi_theap_malloc_small and checking its result. Only the generic routine can fail, so both the OOM check and the m_cs_sz store move to that helper; leaving either at the call site would make the cold call one that has to return, and the fast path would pay a prologue and epilogue on every allocation to serve a path it almost never takes. Restating the callers' precondition on sz (word-aligned, non-zero, at most MI_SMALL_SIZE_MAX) additionally folds mimalloc's bin-index rounding into a plain byte offset and lets the header store drop its mask. That bound is a safety condition in its own right, as a larger sz would index pages_free_direct out of bounds, so it is also asserted.

-push   rbx                            ; frame, only for the generic call below
 mov    rax, [rip+...]                 ; &lean_g_tls
 inc    QWORD PTR fs:[rax]             ; heartbeat++
 mov    rcx, QWORD PTR fs:[rax+0x8]    ; theap
 mov    esi, edi
-lea    rax, [rsi+0x7]                 ; round sz up to a bin index
-and    rax, 0xfffffffffffffff8
-mov    rdx, [rcx+rax*1+0x120]         ; theap->pages_free_direct[..]
+mov    rdx, [rcx+rsi*1+0x120]         ; sz is the byte offset already
 mov    rax, [rdx+0x8]                 ; page->free
 test   rax, rax
-je     .Lcold                         ; inline generic path, ending in pop rbx; ret
+je     lean_alloc_small_generic_core  ; tail jump, nothing to tear down
 mov    rcx, [rax]                     ; pop the block
 mov    [rdx+0x8], rcx
 inc    WORD PTR [rdx+0x10]            ; page->used++
 mov    QWORD PTR [rax], 0x0           ; mimalloc's free-list scrub
-mov    WORD PTR [rax+0x4], di         ; m_cs_sz, masked to 16 bits
-pop    rbx
+mov    DWORD PTR [rax+0x4], edi       ; m_cs_sz, no mask needed
 ret

This PR shortens `lean_alloc_small_object_core` from 18 to 14 instructions by keeping mimalloc's generic allocation path out of line, so that the fast path needs no stack frame.

The entry point now tests `page->free` itself and tail-jumps to a cold helper on a miss, instead of calling `mi_theap_malloc_small` and checking its result. Only the generic routine can fail, so both the OOM check and the `m_cs_sz` store move to that helper; leaving either at the call site would make the cold call one that has to return, and the fast path would pay a prologue and epilogue on every allocation to serve a path it almost never takes. Restating the callers' precondition on `sz` (word-aligned, non-zero, at most `MI_SMALL_SIZE_MAX`) additionally folds mimalloc's bin-index rounding into a plain byte offset and lets the header store drop its mask. That bound is a safety condition in its own right, as a larger `sz` would index `pages_free_direct` out of bounds, so it is also asserted.

```diff
-push   rbx                            ; frame, only for the generic call below
 mov    rax, [rip+...]                 ; &lean_g_tls
 inc    QWORD PTR fs:[rax]             ; heartbeat++
 mov    rcx, QWORD PTR fs:[rax+0x8]    ; theap
 mov    esi, edi
-lea    rax, [rsi+0x7]                 ; round sz up to a bin index
-and    rax, 0xfffffffffffffff8
-mov    rdx, [rcx+rax*1+0x120]         ; theap->pages_free_direct[..]
+mov    rdx, [rcx+rsi*1+0x120]         ; sz is the byte offset already
 mov    rax, [rdx+0x8]                 ; page->free
 test   rax, rax
-je     .Lcold                         ; inline generic path, ending in pop rbx; ret
+je     lean_alloc_small_generic_core  ; tail jump, nothing to tear down
 mov    rcx, [rax]                     ; pop the block
 mov    [rdx+0x8], rcx
 inc    WORD PTR [rdx+0x10]            ; page->used++
 mov    QWORD PTR [rax], 0x0           ; mimalloc's free-list scrub
-mov    WORD PTR [rax+0x4], di         ; m_cs_sz, masked to 16 bits
-pop    rbx
+mov    DWORD PTR [rax+0x4], edi       ; m_cs_sz, no mask needed
 ret
```

Measured by swapping `libleanshared.so` between two full builds, single-threaded with `-DElab.async=false` on pinned cores: `tests/elab_bench/big_omega.lean` goes from 18.781G to 18.650G user instructions (-0.694%) and `import Lean` from 1.9356G to 1.9215G (-0.729%), each reproducible to within 0.02%.

LLVM's hot-cold splitting does not do this: with `-mllvm -hot-cold-split=true` the pass fires 27 times elsewhere in the translation unit and leaves this function byte-identical, because it outlines a cold region into a function the original still calls and returns from, which does not remove the frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kha

Kha commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

!bench

@leanprover-radar

leanprover-radar commented Sep 3, 2026

Copy link
Copy Markdown

Benchmark results for cc86abf against 137a88d are in. There are significant results. @Kha

  • build//instructions: -106.9G (-0.95%)

Large changes (15✅)

  • compiled/binarytrees.st//instructions: -1.2G (-2.32%)
  • compiled/binarytrees//instructions: -1.2G (-2.32%)
  • compiled/deriv//instructions: -120.4M (-1.85%)
  • compiled/hashmap//instructions: -92.3M (-2.85%)
  • compiled/ilean_roundtrip//instructions: -167.4M (-0.77%)
  • compiled/io_compute//instructions: -124.7M (-1.13%)
  • compiled/liasolver//instructions: -27.2M (-0.81%)
  • compiled/phashmap//instructions: -131.8M (-1.51%)
  • compiled/qsort//instructions: -127.4M (-0.84%)
  • compiled/rbmap//instructions: -247.9M (-3.10%)
  • compiled/rbmap_checkpoint2//instructions: -248.7M (-2.91%)
  • compiled/rbmap_fbip//instructions: -227.9M (-3.23%)
  • compiled/rbmap_library//instructions: -163.1M (-1.92%)
  • compiled/treemap//instructions: -144.2M (-0.86%)
  • compiled/unionfind//instructions: -608.3M (-2.87%)

Medium changes (7✅)

  • compiled/parser//instructions: -280.4M (-0.79%)
  • compiled/select//instructions: -46.7M (-1.72%)
  • elab/big_do//instructions: -205.4M (-1.15%)
  • elab/bv_decide_mul//instructions: -238.9M (-0.74%)
  • elab/simp_local//instructions: -341.7M (-1.14%)
  • elab/whnfMatcherImplicitTransparencyCaching//instructions: -163.4M (-0.68%)
  • misc/import Std.Data.DHashMap.Internal.RawLemmas//instructions: -3.2G (-1.51%)

Small changes (1020✅)

  • build/lakeprof/longest rebuild path//instructions: -6.3G (-1.07%)
  • build/module/Init.BinderPredicates//instructions: -22.9M (-1.11%)
  • build/module/Init.CbvSimproc//instructions: -24.8M (-1.20%)
  • build/module/Init.Control.Basic//instructions: -24.6M (-1.16%)
  • build/module/Init.Control.Except//instructions: -13.9M (-1.00%)
  • build/module/Init.Control.Lawful.Basic//instructions: -19.7M (-0.94%)
  • build/module/Init.Control.Lawful.Instances//instructions: -59.0M (-0.90%)
  • build/module/Init.Control.Lawful.MonadAttach//instructions: -3.7M (-0.73%)
  • build/module/Init.Control.Lawful.MonadLift.Instances//instructions: -10.0M (-0.87%)
  • build/module/Init.Control.Lawful.MonadLift//instructions: -4.0M (-0.79%)
  • build/module/Init.Control.State//instructions: -10.4M (-0.96%)
  • build/module/Init.Conv//instructions: -45.6M (-1.24%)
  • build/module/Init.Core//instructions: -97.9M (-1.01%)
  • build/module/Init.Data.Array.Attach//instructions: -103.3M (-1.03%)
  • build/module/Init.Data.Array.Basic//instructions: -103.7M (-0.98%)
  • build/module/Init.Data.Array.BinSearch//instructions: -39.9M (-0.69%)
  • build/module/Init.Data.Array.Count//instructions: -25.9M (-0.96%)
  • build/module/Init.Data.Array.Erase//instructions: -62.8M (-0.90%)
  • build/module/Init.Data.Array.Extract//instructions: -234.8M (-0.71%)
  • build/module/Init.Data.Array.FinRange//instructions: -10.1M (-0.84%)
  • and 999 more
  • and 1 hidden

@github-actions github-actions Bot added the toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN label Sep 3, 2026
@mathlib-lean-pr-testing

Copy link
Copy Markdown

Mathlib CI status (docs):

  • ❗ Batteries/Mathlib CI will not be attempted unless your PR branches off the nightly-with-mathlib branch. Try git rebase 137a88d9d363d6f0c60de75d4cef02a26f38962e --onto c632a0a0e434a951cdcf61bb4da3344abadd5587. You can force Mathlib CI using the force-mathlib-ci label. (2026-09-03 17:02:16)

@leanprover-bot

Copy link
Copy Markdown
Collaborator

Reference manual CI status:

  • ❗ Reference manual CI will not be attempted unless your PR branches off the nightly-with-manual branch. Try git rebase 137a88d9d363d6f0c60de75d4cef02a26f38962e --onto 19c79593c47bb8dd3371327c08fc80775d8488af. You can force reference manual CI using the force-manual-ci label. (2026-09-03 17:02:18)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants