Skip to content

Commit 00e7a80

Browse files
mjp41Copilot
andcommitted
Rewrite docs/Arena.md as a docs/ companion to AddressSpace.md
* Drop the LargeBuddyRange framing (that range no longer exists). * Align the mechanism description with the in-tree code, which builds positive serve masks rather than the inverse skip masks the original sketch used. * Add brief sections on the two-tree structure (one bin tree per non-empty bin + one range tree for coalescing) and on the two reps Arena ships with: PagemapRep behind LargeArenaRange for whole-chunk allocations, InplaceRep behind SmallArenaRange for sub-chunk metadata. * Link out to AddressSpace.md. 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 37b18a8 commit 00e7a80

2 files changed

Lines changed: 112 additions & 84 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: 111 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,152 @@
1-
# Bitmap-Indexed Coalescing Range
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).
210

311
## The problem
412

5-
snmalloc's `LargeBuddyRange` only stores power-of-two blocks. A request for 5
6-
chunks must be served from an 8-chunk buddy block, wasting 3 chunks. We want
7-
to store blocks at their actual size and use snmalloc's full size class
8-
sequence at the range level.
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).
921

10-
## The core idea: search upward, skip a mask
22+
## The core idea: search upward, mask out exceptions
1123

12-
Free blocks are binned by the set of size classes they can serve. To allocate,
13-
search upward through bins — any larger block can be carved down. This almost
14-
works perfectly, but some bins hold blocks whose alignment is too poor to
15-
serve certain smaller, more-aligned sizes. Those bins must be masked out
16-
during the search.
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.
1730

18-
The mechanism: `find_first_set(bitmap & ~skip_mask)`. The skip mask depends
19-
only on the requested size class, not on the block. It's a small constant
20-
that can be precomputed.
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.
2136

22-
## Why skips exist
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
2342

2443
snmalloc's size classes follow `S = 2^e + m · 2^(e−B)`, where `B` is the
25-
number of intermediate bits. Each size class has a natural alignment
26-
`align(S) = S & ~(S−1)`.
44+
mantissa-bit width (`INTERMEDIATE_BITS`, 2 in production). Each size class
45+
has a natural alignment `align(S) = S & -S`.
2746

2847
A size class with high alignment needs padding to reach an aligned address
2948
within a block. A block of a *larger* size class with *lower* alignment may
3049
not have room for that padding. Concretely: a block of size 5 at address 1
31-
can serve size 5 (alignment 1) but cannot serve size 4 (alignment 4) — there
32-
aren't enough chunks left after padding to the first 4-aligned address.
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.
3355

34-
Same size block, different address, different capability. This is what creates
35-
the need for separate bins and skip masks.
56+
## Bin count grows slowly in B
3657

37-
## The general structure
58+
At each exponent, the distinct servable sets are enumerated exhaustively:
3859

39-
At each exponent level, the distinct "servable sets" (which size classes a
40-
block can serve) form a structure with some incomparable pairs. Exhaustive
41-
enumeration shows:
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 |
4266

43-
| B | Mantissas/exponent | Bins/exponent | Max skip mask bits |
44-
|---|-------------------:|--------------:|-------------------:|
45-
| 1 | 2 | 2 | 0 |
46-
| 2 | 4 | 5 | 1 |
47-
| 3 | 8 | 13 | 4 |
48-
| 4 | 16 | 34 | 11 |
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.
4970

50-
Each bin corresponds to a distinct servable set. The bins are ordered so that
51-
upward search is almost always correct — the skip mask handles the exceptions.
71+
## The two-tree structure
5272

53-
For any B, the structure is:
54-
- **Most requests need no skips.** Only size classes with alignment higher
55-
than expected for their position in the sequence need to mask anything.
56-
- **The skip mask is a small constant** per size class, precomputable at
57-
compile time.
58-
- **The mechanism is identical** regardless of B:
59-
`find_first_set(bitmap & ~skip_mask, start_bit)`.
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:
6075

61-
`prototype/skip_analysis.py` verifies this exhaustively for B = 1, 2, 3.
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.
6279

63-
## The bitmap design
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.
6483

65-
Each free block gets one bin based on its size and alignment. Within each
66-
exponent, there are as many bins as there are distinct servable sets (5 for
67-
B=2, 13 for B=3). A flat bitmap tracks which bins are non-empty.
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.
6887

69-
To allocate size class `(e, m)`:
88+
On free: range tree lookup → coalesce with neighbours if their tags allow
89+
→ insert the resulting (possibly merged) block.
7090

71-
1. Compute the **start bit** — the first bin that could serve this size class.
72-
2. Compute the **skip mask** — bits for bins that can't serve this request.
73-
3. `find_first_set(bitmap & ~skip_mask, start_bit)` → pop a block from that
74-
bin.
91+
## Two variants over the same Arena
7592

76-
The returned block may not be exactly aligned for the requested size class.
77-
The caller **carves** the aligned region and returns any prefix/suffix
78-
remainders to the free pool.
93+
`Arena` is parameterised by a **Rep** (representation) that decides where
94+
the per-block tree-node state lives. Two reps ship today:
7995

80-
## Contrast with buddy allocators
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.
81100

82-
A buddy allocator guarantees alignment by construction — a 16-chunk buddy is
83-
always 16-aligned — but wastes space by decomposing everything into
84-
power-of-two pieces.
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.
85109

86-
This design stores blocks at their actual size (no decomposition, no waste)
87-
and handles alignment at allocation time by carving. The skip mask makes
88-
lookup O(1) despite blocks having arbitrary size and alignment.
110+
Both reps drive the same bin / range tree logic in `arena.h`; the bin
111+
classifier and bitmap in `arenabins.h` are shared.
89112

90-
## Concrete example (B = 2)
113+
## Why this matters for metadata
91114

92-
At exponent `e = 2`, the size classes are 4, 5, 6, 7. There are 5 bins,
93-
each labeled by the set of size classes it can serve at this exponent:
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:
94125

95126
Bin 0: serves {4}
96127
Bin 1: serves {5}
97128
Bin 2: serves {4, 5}
98129
Bin 3: serves {4, 5, 6}
99130
Bin 4: serves {4, 5, 6, 7}
100131

101-
Allocation searches upward from the smallest sufficient bin:
102-
103-
Request for 7: can use bin 4 → search bits {4}
104-
Request for 6: can use bins 3, 4 → search bits {3, 4}
105-
Request for 5: can use bins 1, 2, 3, 4 → search bits {1, 2, 3, 4}
106-
Request for 4: can use bins 0, 2, 3, 4 — skip 1 → search bits {0, 2, 3, 4}
107-
108-
Only the request for size 4 needs to skip a bin: bin 1 holds blocks that can
109-
serve 5 but not 4. The skip mask is just bit 1.
110-
111-
## Concrete example (B = 3)
132+
The per-request serve masks (within this exponent — higher exponents
133+
always serve, so their bits are set):
112134

113-
At exponent `e = 4`, the size classes are 16, 18, 20, 22, 24, 26, 28, 30.
114-
There are 13 bins. The skip analysis shows:
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
115139

116-
Request for 16 (align 16): must skip bins for {18}, {20}, {22}, {26}
117-
Request for 24 (align 8): must skip bin for {26}
118-
All other requests: no skips needed
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.
119142

120-
The pattern: size 16 has high alignment and must skip 4 bins whose blocks
121-
are large enough but too poorly aligned. Size 24 is a "sub-power-of-two"
122-
(alignment 8) and must skip 1 bin. All odd-coefficient sizes have low
123-
alignment and never need to skip anything.
143+
## Where to look in the code
124144

125-
Same mechanism, wider mask, same `find_first_set(bitmap & ~mask)` operation.
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)