Skip to content

Commit 3941d18

Browse files
mjp41Copilot
andcommitted
Add docs/Arena.md
A docs/ companion to AddressSpace.md describing the Arena design: * The two-tree structure (one bin tree per non-empty bin + one range tree for coalescing) and how lookup/serve/insert/coalesce flow through it. * The mechanism for building positive serve masks (matching the in-tree code). * The two Rep variants Arena ships with: PagemapRep behind LargeArenaRange for whole-chunk allocations, InplaceRep behind SmallArenaRange for sub-chunk metadata. Also add PLAN.md to .gitignore so contributors can keep a local planning document without committing it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f873074 commit 3941d18

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@
2727

2828
# rust target
2929
/target
30+
PLAN.md

docs/Arena.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# The Arena: A Bitmap-Indexed Coalescing Range
2+
3+
`Arena` is snmalloc's address-space range that stores free blocks at their
4+
**natural** size — no power-of-two rounding — and serves any request from the
5+
full snmalloc size-class sequence. It sits in the per-thread range pipeline
6+
underneath the slab caches and replaces the historical buddy-based ranges.
7+
8+
This document is the conceptual introduction. For where `Arena` plugs into
9+
the wider range chain, see [`AddressSpace.md`](AddressSpace.md).
10+
11+
## The problem
12+
13+
A buddy allocator only stores power-of-two blocks. A request for 5 chunks
14+
must be served from an 8-chunk buddy block, wasting 3 chunks. We wanted a
15+
range that
16+
17+
* stores blocks at their actual size,
18+
* uses snmalloc's full `(exponent, mantissa)` size-class sequence at the
19+
range level, and
20+
* still answers "find a block that can serve this request" in O(1).
21+
22+
## The core idea: search upward, mask out exceptions
23+
24+
Free blocks are binned by the *set of size classes they can serve* — the
25+
**servable set**. To allocate, you walk a per-arena non-empty-bins bitmap
26+
upward through the bins; any larger block can be carved down. This almost
27+
works perfectly. The exception is alignment: some bins hold blocks whose
28+
address alignment is too poor to serve certain smaller, *more* aligned size
29+
classes. Those bins must be excluded from the search for those requests.
30+
31+
The implementation builds the per-request filter *positively* as a **serve
32+
mask** — bit `k` set means bin `k` can serve this request — and the lookup
33+
is `find_first_set(bitmap & serve_mask, start_word)`. The serve mask
34+
depends only on the requested size class, not on the block, so it is
35+
precomputed at compile time.
36+
37+
(The original sketch of this design used the equivalent inverse framing of
38+
a "skip mask" with `bitmap & ~skip_mask`; see `arenabins.h` for the
39+
in-tree explanation of why positive is preferred.)
40+
41+
## Why the exceptions exist
42+
43+
snmalloc's size classes follow `S = 2^e + m · 2^(e−B)`, where `B` is the
44+
mantissa-bit width (`INTERMEDIATE_BITS`, 2 in production). Each size class
45+
has a natural alignment `align(S) = S & -S`.
46+
47+
A size class with high alignment needs padding to reach an aligned address
48+
within a block. A block of a *larger* size class with *lower* alignment may
49+
not have room for that padding. Concretely: a block of size 5 at address 1
50+
can serve size 5 (alignment 1) but cannot serve size 4 (alignment 4) —
51+
there is not enough space after padding to the first 4-aligned address.
52+
53+
Same size block, different address, different servable set. This is why
54+
distinct bins per servable-set are needed.
55+
56+
## Bin count grows slowly in B
57+
58+
At each exponent, the distinct servable sets are enumerated exhaustively:
59+
60+
| B | Mantissas/exponent | Bins/exponent | Max mask bits |
61+
|---|-------------------:|--------------:|--------------:|
62+
| 1 | 2 | 2 | 0 |
63+
| 2 | 4 | 5 | 1 |
64+
| 3 | 8 | 13 | 4 |
65+
| 4 | 16 | 34 | 11 |
66+
67+
Most requests need no exceptions at all. Only size classes whose alignment
68+
exceeds the expected alignment for their position in the sequence have any
69+
bits to mask. The whole structure is constant-folded into a few small tables.
70+
71+
## The two-tree structure
72+
73+
A bitmap alone is not enough — when a bin is non-empty, the arena still has
74+
to *retrieve* and *coalesce* blocks. Each `Arena` therefore maintains:
75+
76+
* **One red-black tree per non-empty bin** (the "bin trees"), keyed by
77+
block address, giving O(log n) selection within a bin. The non-empty-bins
78+
bitmap is the index over these trees.
79+
80+
* **One red-black tree of all free blocks** (the "range tree"), keyed by
81+
address, used to find a block's left/right neighbours for coalescing on
82+
free.
83+
84+
On allocation: bitmap lookup → choose the bin → pop a block from its
85+
bin tree → `carve` returns pre-pad / aligned request / post-pad → pre and
86+
post (if any) re-enter the arena via the bin and range trees.
87+
88+
On free: range tree lookup → coalesce with neighbours if their tags allow
89+
→ insert the resulting (possibly merged) block.
90+
91+
## Two variants over the same Arena
92+
93+
`Arena` is parameterised by a **Rep** (representation) that decides where
94+
the per-block tree-node state lives. Two reps ship today:
95+
96+
* **`PagemapRep`** — node state lives in the pagemap entry that already
97+
covers the block. Used by **`LargeArenaRange`**, which manages whole
98+
chunks and larger. Node access is a pagemap lookup; no in-band space is
99+
consumed.
100+
101+
* **`InplaceRep`** — node state lives *in the free block itself*, in the
102+
first units. Used by **`SmallArenaRange`**, which manages sub-chunk
103+
metadata fragments where no pagemap entry exists for the fragment. The
104+
layout packs the bin tree pointers, the range tree pointers, and (for
105+
blocks ≥ 3 units) a large-size word into the leading units of the free
106+
block. Unit size is `next_pow2(2 · sizeof(CapPtr))` — 16 B without
107+
CHERI, 32 B with pure-capability CHERI/Morello — large enough to hold
108+
the two pointers a free block must store.
109+
110+
Both reps drive the same bin / range tree logic in `arena.h`; the bin
111+
classifier and bitmap in `arenabins.h` are shared.
112+
113+
## Why this matters for metadata
114+
115+
Slab metadata typically wants a pow2 client structure (e.g. a 128 B
116+
bitmap) plus a fixed ~32 B header. A buddy-based small range rounds
117+
`160 B → 256 B` (96 B wasted per slab). `SmallArenaRange` rounds to a unit
118+
multiple (`MIN_META_ALIGN`), so the same allocation costs ~160 B. Across
119+
many slabs and large heaps this is real memory.
120+
121+
## Concrete example (B = 2, in-production)
122+
123+
At exponent `e = 2` the size classes are 4, 5, 6, 7, and there are 5 bins,
124+
each labeled by the set of sizes it can serve at this exponent:
125+
126+
Bin 0: serves {4}
127+
Bin 1: serves {5}
128+
Bin 2: serves {4, 5}
129+
Bin 3: serves {4, 5, 6}
130+
Bin 4: serves {4, 5, 6, 7}
131+
132+
The per-request serve masks (within this exponent — higher exponents
133+
always serve, so their bits are set):
134+
135+
Request for 7: serve bins {4}
136+
Request for 6: serve bins {3, 4}
137+
Request for 5: serve bins {1, 2, 3, 4}
138+
Request for 4: serve bins {0, 2, 3, 4} — bin 1 holds only {5} blocks
139+
140+
Only the size-4 request has an exception: bin 1 must not be picked. All
141+
other requests get the simple "everything at or above" mask.
142+
143+
## Where to look in the code
144+
145+
* `src/snmalloc/backend_helpers/arenabins.h` — bin classification, serve
146+
masks, the non-empty-bins bitmap, the `carve` primitive.
147+
* `src/snmalloc/backend_helpers/arena.h` — bin-tree-per-bin + range-tree
148+
structure, allocation and free / coalesce paths.
149+
* `src/snmalloc/backend_helpers/largearenarange.h``Arena<PagemapRep>`
150+
for whole-chunk allocations.
151+
* `src/snmalloc/backend_helpers/smallarenarange.h`,
152+
`inplacerep.h``Arena<InplaceRep>` for sub-chunk metadata.

0 commit comments

Comments
 (0)