Skip to content

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

Draft
Kha wants to merge 1 commit into
masterfrom
heartbeat-minimal
Draft

perf: make the small-object allocation fast path a leaf#14998
Kha wants to merge 1 commit into
masterfrom
heartbeat-minimal

Conversation

@Kha

@Kha Kha commented Sep 2, 2026

Copy link
Copy Markdown
Member

This PR shortens the fused small-object allocation entry point from 18 to 14 instructions and gives constructor allocation a single merged header store, worth 0.75% of user instructions on the workloads measured below.

Three changes, none of which need a patch to mimalloc. The page refill moves into a cold helper that the entry points tail-jump to, so the fast path no longer builds a stack frame for a call it rarely makes; the helper also writes m_cs_sz, which is what leaves sz dead across the branch and keeps lean_alloc_small_object_core a leaf as well. Restating the callers' precondition on sz (word-aligned, non-zero, at most MI_SMALL_SIZE_MAX) folds mimalloc's bin-index rounding into a plain byte offset and drops the mask before the header store; the bound is a safety condition in its own right, since a larger sz would index pages_free_direct out of bounds. Finally, a second entry point lean_alloc_small_object_raw leaves the whole header to the caller.

The fast path of lean_alloc_small_object_core:

-push   rbx                            ; frame, only for the refill 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 cold 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

lean_alloc_small_object_raw is what lets lean_alloc_ctor write all four header fields with adjacent stores that merge, rather than having the callee store m_cs_sz and the caller fill in around it. Allocating a List.cons:

 mov    edi, 0x18
-call   lean_alloc_small_object_core   ; stores m_cs_sz itself
-mov    DWORD PTR [rax], 0x1           ; m_rc
-mov    WORD PTR [rax+0x6], 0x102      ; m_other, m_tag; cannot merge across m_cs_sz
+call   lean_alloc_small_object_raw    ; leaves the header alone
+movabs rcx, 0x102001800000001         ; m_rc | m_cs_sz | m_other | m_tag
+mov    QWORD PTR [rax], rcx           ; one store
 mov    QWORD PTR [rax+0x8], r14
 mov    QWORD PTR [rax+0x10], rbx

Measured single-threaded with -DElab.async=false on pinned cores: tests/elab_bench/big_omega.lean goes from 18.946G to 18.803G user instructions (-0.754%) and import Lean from 1.9365G to 1.9219G (-0.753%), each reproducible to within 0.01%; cycles on the former fall about 1.2%. Heartbeat counts are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

@Kha
Kha marked this pull request as draft September 2, 2026 11:59
@Kha

Kha commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

!bench

@leanprover-radar

leanprover-radar commented Sep 2, 2026

Copy link
Copy Markdown

Benchmark results for 29fe74e against ac0f56e are in. There are significant results. @Kha

  • build//instructions: -196.0G (-1.72%)

Large changes (19✅, 1🟥)

  • compiled/binarytrees.st//instructions: -3.4G (-6.15%)
  • compiled/binarytrees//instructions: -3.4G (-6.17%)
  • compiled/deriv//instructions: -149.0M (-2.27%)
  • compiled/hashmap//instructions: -126.0M (-3.92%)
  • compiled/ilean_roundtrip//instructions: -255.5M (-1.16%)
  • compiled/io_compute//instructions: -161.9M (-1.47%)
  • compiled/liasolver//instructions: -44.9M (-1.34%)
  • compiled/parser//instructions: -355.7M (-0.99%)
  • compiled/phashmap//instructions: -161.3M (-1.84%)
  • compiled/qsort//instructions: -159.1M (-1.04%)
  • compiled/rbmap//instructions: -256.4M (-3.21%)
  • compiled/rbmap_checkpoint2//instructions: -257.2M (-3.01%)
  • 🟥 compiled/rbmap_fbip//instructions: +69.8M (+1.05%)
  • compiled/rbmap_library//instructions: -1.0G (-10.88%)
  • compiled/sigmaIterator//instructions: -60.3M (-2.25%)
  • compiled/treemap//instructions: -613.6M (-3.57%)
  • compiled/unionfind//instructions: -830.5M (-3.88%)
  • elab/big_do//instructions: -580.0M (-3.17%)
  • size/compile/.out//bytes: -74MiB (-2.66%)
  • size/libleanshared.so//bytes: -3MiB (-1.34%)

Medium changes (32✅)

  • build/module/Init.Data.BitVec.Lemmas//instructions: -1.6G (-1.39%)
  • build/module/Lake.Build.Module//instructions: -1.2G (-2.89%)
  • build/module/Lean.Elab.Do.Legacy//instructions: -1.1G (-2.38%) (reduced significance based on absolute threshold)
  • build/module/Lean.Meta.Tactic.FunInd//instructions: -1.0G (-2.78%)
  • build/module/Std.Data.DHashMap.Internal.RawLemmas//instructions: -5.3G (-2.16%)
  • build/module/Std.Data.DHashMap.RawLemmas//instructions: -2.5G (-1.68%)
  • build/module/Std.Data.DTreeMap.Internal.Lemmas//instructions: -3.3G (-1.53%)
  • build/module/Std.Data.DTreeMap.Raw.Lemmas//instructions: -1.0G (-1.57%)
  • build/module/Std.Data.Internal.List.Associative//instructions: -1.2G (-1.57%)
  • build/module/Std.Tactic.BVDecide.Bitblast.BVExpr.Circuit.Lemmas.Expr//instructions: -1.2G (-1.41%)
  • compiled/select//instructions: -66.7M (-2.43%)
  • compiled/workspaceSymbolsNewRanges//instructions: -11.2M (-1.69%)
  • elab/big_match//instructions: -120.2M (-1.21%)
  • elab/big_match_nat_split//instructions: -165.0M (-1.65%)
  • elab/big_match_partial//instructions: -186.7M (-1.37%)
  • elab/big_omega//instructions: -241.2M (-1.23%)
  • elab/big_omega_MT//instructions: -241.1M (-1.22%)
  • elab/bv_decide_mul//instructions: -334.9M (-1.04%)
  • elab/cbv_arm_ldst//instructions: -799.1M (-1.47%)
  • elab/cbv_system_f//instructions: -1.1G (-1.21%)
  • and 12 more

Small changes (1855✅, 1🟥)

  • build//task-clock: -33s (-1.66%)
  • build//wall-clock: -2s (-2.03%)
  • build/lakeprof/longest rebuild path//instructions: -11.8G (-1.97%)
  • build/module/Init.BinderPredicates//instructions: -43.1M (-2.05%) (reduced significance based on absolute threshold)
  • build/module/Init.ByCases//instructions: -13.2M (-1.66%)
  • build/module/Init.CbvSimproc//instructions: -38.9M (-1.87%)
  • build/module/Init.Control.Basic//instructions: -43.2M (-2.01%) (reduced significance based on absolute threshold)
  • build/module/Init.Control.EState//instructions: -12.3M (-1.75%)
  • build/module/Init.Control.Except//instructions: -25.8M (-1.82%)
  • build/module/Init.Control.ExceptCps//instructions: -17.9M (-1.63%)
  • build/module/Init.Control.Lawful.Basic//instructions: -32.6M (-1.54%)
  • build/module/Init.Control.Lawful.Instances//instructions: -104.6M (-1.57%)
  • build/module/Init.Control.Lawful.MonadAttach.Instances//instructions: -21.8M (-1.34%)
  • build/module/Init.Control.Lawful.MonadAttach.Lemmas//instructions: -15.6M (-1.54%)
  • build/module/Init.Control.Lawful.MonadAttach//instructions: -6.3M (-1.23%)
  • build/module/Init.Control.Lawful.MonadLift.Instances//instructions: -17.5M (-1.51%)
  • build/module/Init.Control.Lawful.MonadLift.Lemmas//instructions: -13.7M (-1.57%)
  • build/module/Init.Control.Lawful.MonadLift//instructions: -6.0M (-1.19%)
  • build/module/Init.Control.Option//instructions: -15.9M (-1.83%)
  • build/module/Init.Control.State//instructions: -20.4M (-1.87%)
  • and 1835 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 2, 2026
@leanprover-bot

leanprover-bot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Reference manual CI status:

  • ❗ Reference manual CI can not be attempted yet, as the nightly-testing-2026-08-30 tag does not exist there yet. We will retry when you push more commits. If you rebase your branch onto nightly-with-manual, reference manual CI should run now. You can force reference manual CI using the force-manual-ci label. (2026-09-02 12:19:17)
  • ❗ 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 14:52:04)

@github-actions github-actions Bot added the mathlib4-nightly-available A branch for this PR exists at leanprover-community/mathlib4-nightly-testing:lean-pr-testing-NNNN label Sep 2, 2026
@mathlib-lean-pr-testing mathlib-lean-pr-testing Bot added the builds-mathlib CI has verified that Mathlib builds against this PR label Sep 2, 2026
@mathlib-lean-pr-testing

mathlib-lean-pr-testing Bot commented Sep 2, 2026

Copy link
Copy Markdown

Mathlib CI status (docs):

  • ✅ Mathlib branch lean-pr-testing-14998 has successfully built against this PR. (2026-09-02 13:25:52) View Log
  • ❗ 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 14:52:02)

@Kha

Kha commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

!bench mathlib

@leanprover-radar

leanprover-radar commented Sep 2, 2026

Copy link
Copy Markdown

Benchmark results for leanprover-community/mathlib4-nightly-testing@81601f8 against leanprover-community/mathlib4-nightly-testing@2415714 are in. There are significant results. @Kha

  • build//instructions: -1.6T (-1.13%)

Large changes (1✅)

  • 1 hidden

Medium changes (1✅)

  • build/module/Mathlib.Geometry.Convex.ConvexSpace.Module//instructions: -1.1G (-1.90%)

Small changes (725✅)

  • build/module/Aesop.BaseM//instructions: -20.4M (-1.27%)
  • build/module/Aesop.Builder.Apply//instructions: -23.7M (-1.38%)
  • build/module/Aesop.Builder.Basic//instructions: -26.1M (-1.44%)
  • build/module/Aesop.Builder.Cases//instructions: -23.7M (-1.33%)
  • build/module/Aesop.Builder.Constructors//instructions: -22.1M (-1.37%)
  • build/module/Aesop.Builder.Default//instructions: -23.9M (-1.49%)
  • build/module/Aesop.Builder.Forward//instructions: -119.1M (-2.32%) (reduced significance based on absolute threshold)
  • build/module/Aesop.Builder.NormSimp//instructions: -26.8M (-1.49%)
  • build/module/Aesop.Builder.Tactic//instructions: -28.7M (-1.53%)
  • build/module/Aesop.Builder.Unfold//instructions: -24.6M (-1.45%)
  • build/module/Aesop.BuiltinRules.ApplyHyps//instructions: -35.8M (-1.59%)
  • build/module/Aesop.BuiltinRules.Assumption//instructions: -48.9M (-1.83%)
  • build/module/Aesop.BuiltinRules.DestructProducts//instructions: -48.3M (-1.60%)
  • build/module/Aesop.BuiltinRules.Ext//instructions: -30.1M (-1.54%)
  • build/module/Aesop.BuiltinRules.Intros//instructions: -24.7M (-1.34%)
  • build/module/Aesop.BuiltinRules.Rfl//instructions: -19.4M (-1.24%)
  • build/module/Aesop.BuiltinRules.Split//instructions: -27.8M (-1.36%)
  • build/module/Aesop.BuiltinRules.Subst//instructions: -41.6M (-1.56%)
  • build/module/Aesop.Check//instructions: -27.3M (-1.59%)
  • build/module/Aesop.EMap//instructions: -52.9M (-1.70%)
  • and 705 more

This PR shortens the fused small-object allocation entry point from 18 to 14 instructions and gives constructor allocation a single merged header store, worth 0.75% of user instructions on the workloads measured below.

Three changes, none of which need a patch to mimalloc. The page refill moves into a cold helper that the entry points tail-jump to, so the fast path no longer builds a stack frame for a call it rarely makes; the helper also writes `m_cs_sz`, which is what leaves `sz` dead across the branch and keeps `lean_alloc_small_object_core` a leaf as well. Restating the callers' precondition on `sz` (word-aligned, non-zero, at most `MI_SMALL_SIZE_MAX`) folds mimalloc's bin-index rounding into a plain byte offset and drops the mask before the header store; the bound is a safety condition in its own right, since a larger `sz` would index `pages_free_direct` out of bounds. Finally, a second entry point `lean_alloc_small_object_raw` leaves the whole header to the caller.

The fast path of `lean_alloc_small_object_core`:

```diff
-push   rbx                            ; frame, only for the refill 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 cold 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
```

`lean_alloc_small_object_raw` is what lets `lean_alloc_ctor` write all four header fields with adjacent stores that merge, rather than having the callee store `m_cs_sz` and the caller fill in around it. Allocating a `List.cons`:

```diff
 mov    edi, 0x18
-call   lean_alloc_small_object_core   ; stores m_cs_sz itself
-mov    DWORD PTR [rax], 0x1           ; m_rc
-mov    WORD PTR [rax+0x6], 0x102      ; m_other, m_tag; cannot merge across m_cs_sz
+call   lean_alloc_small_object_raw    ; leaves the header alone
+movabs rcx, 0x102001800000001         ; m_rc | m_cs_sz | m_other | m_tag
+mov    QWORD PTR [rax], rcx           ; one store
 mov    QWORD PTR [rax+0x8], r14
 mov    QWORD PTR [rax+0x10], rbx
```

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.946G to 18.803G user instructions (-0.754%) and `import Lean` from 1.9365G to 1.9219G (-0.753%), each reproducible to within 0.01%; cycles on the former fall about 1.2%. Heartbeat counts are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kha
Kha force-pushed the heartbeat-minimal branch from 29fe74e to f193bda Compare September 3, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

builds-mathlib CI has verified that Mathlib builds against this PR mathlib4-nightly-available A branch for this PR exists at leanprover-community/mathlib4-nightly-testing:lean-pr-testing-NNNN 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