inillucent_alloc/lib.rs
1//! A size-classed free list over the system allocator.
2//!
3//! Invariant: **it recycles rather than accumulating.** A bump arena that never
4//! frees is the shortest thing to write and the wrong thing to ship: thirty
5//! rounds of the gate's plan would grow it without bound, and what it measured
6//! would be page faults rather than allocation. This keeps one intrusive list
7//! per size class, capped, and hands a block back to the system allocator when
8//! the class is full.
9//!
10//! ## Why the engine has one at all
11//!
12//! Because allocation is where a compile goes. A trivial compile was measured
13//! at **25 heap allocations, with the Windows C runtime heap at 59% of the
14//! time**, and a size-classed free list at **17% overall** on the same
15//! plan - which is why Phase 3's Part E names it the cheapest first move rather
16//! than one of the several structural changes beside it. The same shape is
17//! visible outside compilation: `CREATE INDEX` over a hundred thousand rows
18//! builds two allocations per row just to hold the key and the rowid, and the
19//! gate's `schema.index` spends more time in its scan than SQLite spends on the
20//! whole statement.
21//!
22//! What it removes is exactly what was in question: the size lookup, the
23//! locking and the per-call bookkeeping the system allocator does. It does not
24//! try to be a better allocator in general - above [`LARGEST`] and for any
25//! alignment the system's own guarantee does not cover, the request is
26//! forwarded unchanged.
27//!
28//! ## Why it never allocates
29//!
30//! An allocator that allocates re-enters itself, and a re-entrant allocator is
31//! a deadlock or a stack overflow waiting for the right allocation pattern. So
32//! the free lists are **intrusive**: a freed block holds the pointer to the
33//! next free block of its class in its own first eight bytes, and the heads
34//! live in a fixed-size array of `Cell`s in thread-local storage. Nothing here
35//! calls `Vec`, `Box`, or anything that could.
36//!
37//! ## Why it is a crate of its own
38//!
39//! Because every other production crate in this workspace carries
40//! `#![forbid(unsafe_code)]`, and an allocator cannot. Putting it here keeps
41//! that true everywhere it is true today and confines the unsafe to one file
42//! that has nothing else in it - no dependencies, first-party or otherwise, so
43//! there is nothing it could re-enter itself through.
44//!
45//! ## Why it is per thread
46//!
47//! Because a shared list needs a lock, and the lock is most of what this exists
48//! to remove. A block allocated on one thread and freed on another goes onto
49//! the freeing thread's list, which is safe - the block is memory of a known
50//! class, and the class is derived from the layout the caller hands back - and
51//! at worst moves a block between threads. The per-class cap bounds what that
52//! can cost.
53
54#![deny(missing_docs)]
55// **The one production crate in this workspace allowed to write `unsafe`, and
56// it was outside every check until task-1932 (H9).** It was not in `GOVERNED`
57// and not in `UNSAFE_CRATES`, which is not the same as being permitted: it
58// means nothing read it. A `GlobalAlloc` is an unsafe trait and this crate is
59// the boundary, so the four lints below are what say that the *rest* of it -
60// the size-class arithmetic, the caps, the thread-local lists - is ordinary
61// safe code held to the same standard as the engine.
62#![deny(clippy::indexing_slicing)]
63#![deny(clippy::unwrap_used)]
64#![deny(clippy::expect_used)]
65#![deny(clippy::panic)]
66#![cfg_attr(
67 test,
68 allow(
69 clippy::expect_used,
70 clippy::indexing_slicing,
71 clippy::panic,
72 clippy::unwrap_used
73 )
74)]
75
76use std::alloc::{GlobalAlloc, Layout, System};
77use std::cell::Cell;
78
79/// The largest allocation the free list handles itself.
80///
81/// Above this, up to 64 KiB, a block comes from one of four page sized lists
82/// shared by every thread; see [`big_class_of`]. Above 64 KiB the system
83/// allocator is asked directly.
84pub const LARGEST: usize = 4_096;
85
86/// The granularity of a size class, which is also the alignment every pooled
87/// block is made with.
88const GRAIN: usize = 16;
89
90/// How many size classes the free list holds.
91///
92/// One per sixteen bytes up to [`LARGEST`], which is the granularity a `Vec<u8>`
93/// of a row, a key or a name actually lands on.
94const CLASSES: usize = LARGEST / GRAIN + 1;
95
96/// How many **bytes** one class keeps before handing the rest back.
97///
98/// The cap is what makes this a recycler rather than a leak: a workload that
99/// allocates a million blocks of one class and frees them all keeps a bounded
100/// amount and returns the rest, so the process's footprint is bounded by the
101/// classes rather than by the workload.
102///
103/// **Bytes rather than blocks.** The cap was a thousand and
104/// twenty-four *blocks* per class - a fixed count over classes whose sizes
105/// differ by two hundred and fifty-six times, so the same number meant sixteen
106/// kilobytes in the smallest class and four megabytes in the largest. The
107/// gate's write family paid for it: one round left 5.2 MiB of heap standing
108/// that nothing live was using, and the process's high-water mark is exactly
109/// what this ticket's memory bar reads.
110///
111/// Sixteen kilobytes per class keeps the small classes as deep as they were -
112/// the sixteen-byte class still holds its thousand and twenty-four blocks,
113/// which is where the free list's measured speed comes from - and bounds the
114/// largest at four. The whole cache is at most `CLASSES * 16 KiB`, about
115/// 4 MiB, rather than an unbounded function of which classes a workload
116/// happened to touch.
117const PER_CLASS_BYTES: usize = 64 << 10;
118
119/// The block cap that was here before, kept as a ceiling.
120///
121/// **The byte cap only ever takes retention away.** Applying it alone would
122/// have made the smallest class keep four thousand blocks where it used to keep
123/// a thousand - more, not less - and the small classes are exactly where the
124/// free list's measured speed comes from. Keeping the old count as a ceiling
125/// means every class holds *at most* what it held before, and the large ones
126/// hold far less.
127const PER_CLASS_BLOCKS: usize = 1_024;
128
129/// How many blocks of each class the free list keeps, worked out once.
130///
131/// **A table rather than the arithmetic, because `dealloc` is the hot path.**
132/// The cap is a division and a pair of clamps, and computing it per free put a
133/// divide on every deallocation the program makes. It is a constant of the
134/// class, so it is a constant of the build.
135const CAPS: [usize; CLASSES] = caps();
136
137/// Builds [`CAPS`] at compile time.
138///
139/// The lower of the two caps, and at least one so no class is barred from
140/// recycling by arithmetic. At 64 KiB and a 1,024-block ceiling: the 16-byte
141/// class keeps its full thousand and twenty-four, unchanged, and the
142/// 4,096-byte class keeps sixteen where it used to keep a thousand - which is
143/// four megabytes of one class's free list that a write workload was leaving
144/// standing.
145#[allow(clippy::indexing_slicing)]
146const fn caps() -> [usize; CLASSES] {
147 let mut caps = [1usize; CLASSES];
148 let mut class = 0usize;
149 while class < CLASSES {
150 let size = if class * GRAIN > GRAIN {
151 class * GRAIN
152 } else {
153 GRAIN
154 };
155 let mut cap = PER_CLASS_BYTES / size;
156 if cap > PER_CLASS_BLOCKS {
157 cap = PER_CLASS_BLOCKS;
158 }
159 if cap < 1 {
160 cap = 1;
161 }
162 caps[class] = cap;
163 class += 1;
164 }
165 caps
166}
167
168/// Returns how many blocks of one class the free list keeps.
169///
170/// @param class - the size class
171#[inline]
172fn per_class(class: usize) -> usize {
173 CAPS.get(class).copied().unwrap_or(1)
174}
175
176thread_local! {
177 /// The head of each class's intrusive free list, or null.
178 ///
179 /// `const` on the whole initialiser, not only on the elements: it removes
180 /// the lazy-initialisation check from every access, and an allocator's
181 /// per-call cost is the thing this crate exists to keep small.
182 static HEADS: [Cell<*mut u8>; CLASSES] =
183 const { [const { Cell::new(std::ptr::null_mut()) }; CLASSES] };
184 /// How many blocks each class is holding.
185 static HELD: [Cell<usize>; CLASSES] = const { [const { Cell::new(0) }; CLASSES] };
186}
187
188/// Returns the size class an allocation falls in, if any.
189///
190/// `None` means "not ours": too large, too aligned, or empty.
191///
192/// **A request under eight bytes is the sixteen byte class's (task-2191).** It
193/// used to be refused, as too small to hold the link a freed block stores in
194/// itself. But the block a class hands out is always [`layout_of`] the class,
195/// sixteen bytes at least, so the link fits whatever was asked for. Refusing
196/// sent every short text value, such as `t7` or `3.5`, to the system heap one
197/// allocation and one free at a time. In a profile of forty `.import` runs of
198/// 50,000 rows, the system heap's allocations and frees were 29% of the
199/// samples, and they are gone from the same profile with this.
200///
201/// @param layout - the allocation's layout
202#[inline]
203fn class_of(layout: Layout) -> Option<usize> {
204 if layout.size() > LARGEST || layout.align() > GRAIN || layout.size() == 0 {
205 return None;
206 }
207 Some(layout.size().div_ceil(GRAIN))
208}
209
210/// Returns the layout a size class's blocks are allocated with.
211///
212/// @param class - the size class
213#[inline]
214fn layout_of(class: usize) -> Layout {
215 // Every class is a multiple of the grain and aligned to it, so a block is
216 // always at least as large and as aligned as any request in its class.
217 Layout::from_size_align(class.saturating_mul(GRAIN).max(GRAIN), GRAIN)
218 .unwrap_or_else(|_| Layout::new::<u128>())
219}
220
221/// The smallest block the page sized lists hold.
222const BIG_SMALLEST: usize = 8 << 10;
223
224/// How many page sized classes there are: 8, 16, 32 and 64 KiB.
225const BIG_CLASSES: usize = 4;
226
227/// How many bytes one page sized class keeps before handing the rest back.
228///
229/// Half a megabyte: eight 64 KiB blocks, up to sixty four 8 KiB ones, and at
230/// most 2 MiB across the four classes.
231const BIG_PER_CLASS_BYTES: usize = 512 << 10;
232
233/// Returns the page sized class an allocation above [`LARGEST`] falls in.
234///
235/// **Page sized blocks are asked for again and again (task-2191).** A leaf is
236/// 32 KiB, and a write copies it, encodes a new image of it and reads its rows
237/// into buffers of about its size, then frees all of them. The doc comment on
238/// [`LARGEST`] assumed a big block is rare; on an insert into an indexed table
239/// they were the most frequent allocation the system heap still served, and
240/// the Windows heap returns a freed block of that size to the operating system
241/// and commits it again on the next request, so each one also cost page
242/// faults. A request is rounded up to a power of two, so a buffer that grows by
243/// doubling stays in its block.
244///
245/// @param layout - the allocation's layout
246#[inline]
247fn big_class_of(layout: Layout) -> Option<usize> {
248 let largest = BIG_SMALLEST << (BIG_CLASSES - 1);
249 if layout.size() <= LARGEST || layout.size() > largest || layout.align() > GRAIN {
250 return None;
251 }
252 let rounded = layout.size().next_power_of_two().max(BIG_SMALLEST);
253 Some((rounded.trailing_zeros() - BIG_SMALLEST.trailing_zeros()) as usize)
254}
255
256/// Returns the layout a page sized class's blocks are allocated with.
257///
258/// @param class - the page sized class
259#[inline]
260fn big_layout_of(class: usize) -> Layout {
261 Layout::from_size_align(BIG_SMALLEST << class.min(BIG_CLASSES - 1), GRAIN)
262 .unwrap_or_else(|_| Layout::new::<u128>())
263}
264
265/// The head of each page sized class's free list, shared by every thread.
266static BIG_HEADS: [std::sync::atomic::AtomicPtr<u8>; BIG_CLASSES] =
267 [const { std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()) }; BIG_CLASSES];
268/// How many blocks each page sized class holds.
269static BIG_HELD: [std::sync::atomic::AtomicUsize; BIG_CLASSES] =
270 [const { std::sync::atomic::AtomicUsize::new(0) }; BIG_CLASSES];
271/// One lock per page sized class, held only while a head and its count change.
272static BIG_LOCKS: [std::sync::atomic::AtomicBool; BIG_CLASSES] =
273 [const { std::sync::atomic::AtomicBool::new(false) }; BIG_CLASSES];
274
275/// Runs `work` with one page sized class's lock held.
276///
277/// One list for every thread and every allocator in this file, because these
278/// blocks are few and taking a lock costs little next to filling 32 KiB.
279///
280/// @param class - the page sized class
281/// @param work - what to do while the lock is held
282#[inline]
283fn with_big_lock<R>(class: usize, work: impl FnOnce() -> R) -> Option<R> {
284 use std::sync::atomic::Ordering;
285 let lock = BIG_LOCKS.get(class)?;
286 while lock
287 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
288 .is_err()
289 {
290 std::hint::spin_loop();
291 }
292 let result = work();
293 lock.store(false, Ordering::Release);
294 Some(result)
295}
296
297/// Allocates a block too large for the small classes: from a page sized list
298/// when one fits, or from the system.
299///
300/// @param layout - the allocation's layout
301///
302/// # Safety
303///
304/// As for [`GlobalAlloc::alloc`].
305#[inline]
306unsafe fn big_alloc(layout: Layout) -> *mut u8 {
307 use std::sync::atomic::Ordering;
308 let Some(class) = big_class_of(layout) else {
309 // SAFETY: forwarded unchanged to the system allocator.
310 return unsafe { System.alloc(layout) };
311 };
312 if let (Some(head), Some(held)) = (BIG_HEADS.get(class), BIG_HELD.get(class)) {
313 if !head.load(Ordering::Relaxed).is_null() {
314 let taken = with_big_lock(class, || {
315 let block = head.load(Ordering::Relaxed);
316 if block.is_null() {
317 return block;
318 }
319 // SAFETY: the block is on the list, so its first word is a link.
320 let next = unsafe { block.cast::<*mut u8>().read() };
321 head.store(next, Ordering::Relaxed);
322 held.store(
323 held.load(Ordering::Relaxed).saturating_sub(1),
324 Ordering::Relaxed,
325 );
326 block
327 })
328 .unwrap_or(std::ptr::null_mut());
329 if !taken.is_null() {
330 return taken;
331 }
332 }
333 }
334 // SAFETY: a fresh block of the class's own layout, freed with it in
335 // `big_dealloc`.
336 unsafe { System.alloc(big_layout_of(class)) }
337}
338
339/// Frees a block [`big_alloc`] made: onto its page sized list while the list
340/// is under its cap, or back to the system.
341///
342/// @param pointer - the block
343/// @param layout - the layout it was asked for with
344///
345/// # Safety
346///
347/// As for [`GlobalAlloc::dealloc`], for a block from [`big_alloc`].
348#[inline]
349unsafe fn big_dealloc(pointer: *mut u8, layout: Layout) {
350 use std::sync::atomic::Ordering;
351 let Some(class) = big_class_of(layout) else {
352 // SAFETY: forwarded unchanged to the allocator that made it.
353 return unsafe { System.dealloc(pointer, layout) };
354 };
355 let cap = BIG_PER_CLASS_BYTES / big_layout_of(class).size();
356 let kept = match (BIG_HEADS.get(class), BIG_HELD.get(class)) {
357 (Some(head), Some(held)) => with_big_lock(class, || {
358 let count = held.load(Ordering::Relaxed);
359 if count >= cap {
360 return false;
361 }
362 // SAFETY: the caller has freed the block, so its first word is ours.
363 unsafe {
364 pointer
365 .cast::<*mut u8>()
366 .write(head.load(Ordering::Relaxed))
367 };
368 head.store(pointer, Ordering::Relaxed);
369 held.store(count.saturating_add(1), Ordering::Relaxed);
370 true
371 })
372 .unwrap_or(false),
373 _ => false,
374 };
375 if !kept {
376 // SAFETY: freed with the layout `big_alloc` made it with.
377 unsafe { System.dealloc(pointer, big_layout_of(class)) };
378 }
379}
380
381/// Whether a realloc can keep the block it has: the old and new sizes fall in
382/// the same small class, or in the same page sized class.
383///
384/// @param layout - the block's layout
385/// @param new_size - the size asked for
386#[inline]
387fn same_block(layout: Layout, new_size: usize) -> bool {
388 let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
389 return false;
390 };
391 match (class_of(layout), class_of(wanted)) {
392 (Some(old), Some(new)) => old == new,
393 (None, None) => {
394 let old = big_class_of(layout);
395 old.is_some() && old == big_class_of(wanted)
396 }
397 _ => false,
398 }
399}
400
401/// Whether neither size of a realloc is one this file keeps, so the system
402/// allocator made the block and can grow it in place.
403///
404/// @param layout - the block's layout
405/// @param new_size - the size asked for
406#[inline]
407fn neither_kept(layout: Layout, new_size: usize) -> bool {
408 let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
409 return false;
410 };
411 class_of(layout).is_none()
412 && class_of(wanted).is_none()
413 && big_class_of(layout).is_none()
414 && big_class_of(wanted).is_none()
415}
416
417/// A size-classed free list over the system allocator.
418///
419/// Install it in a binary with
420///
421/// ```ignore
422/// #[global_allocator]
423/// static ALLOCATOR: inillucent_base::alloc::Pooled = inillucent_base::alloc::Pooled;
424/// ```
425///
426/// It is per binary rather than per library because only a binary can name a
427/// global allocator, and because the choice belongs to whoever is running the
428/// program.
429pub struct Pooled;
430
431// SAFETY: every path either forwards to the system allocator unchanged, or
432// hands back a block this allocator obtained from `System` with its class's own
433// layout and has not handed out since. The class is derived from the layout on
434// both sides, so a block is only ever reused for a request it is large enough
435// and aligned enough for.
436unsafe impl GlobalAlloc for Pooled {
437 // SAFETY: a pooled block was allocated by `System.alloc` with the class's
438 // layout, which is at least this request's size and alignment. The link
439 // read out of the block was written by `dealloc` below and nothing has
440 // touched the block since - it is not reachable by any other path while it
441 // is on the list.
442 #[inline]
443 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
444 let Some(class) = class_of(layout) else {
445 // SAFETY: as for this function; `big_alloc` forwards anything it
446 // does not keep to the system allocator unchanged.
447 return unsafe { big_alloc(layout) };
448 };
449 // `try_with`, because a thread tearing down has already dropped its
450 // storage and a panic inside an allocator aborts the process. A request
451 // that finds no list is simply a fresh block.
452 let taken = HEADS
453 .try_with(|heads| {
454 let Some(head) = heads.get(class) else {
455 return std::ptr::null_mut();
456 };
457 let block = head.get();
458 if block.is_null() {
459 return std::ptr::null_mut();
460 }
461 // SAFETY: the block is one this allocator made and put on the
462 // list, and its first word is the link `dealloc` wrote.
463 let next = unsafe { block.cast::<*mut u8>().read() };
464 head.set(next);
465 let _ = HELD.try_with(|held| {
466 if let Some(count) = held.get(class) {
467 count.set(count.get().saturating_sub(1));
468 }
469 });
470 block
471 })
472 .unwrap_or(std::ptr::null_mut());
473 if !taken.is_null() {
474 return taken;
475 }
476 // SAFETY: a fresh block of the class's own layout, which is at least as
477 // large and as aligned as the request. It is freed with the same
478 // layout, in `dealloc` below.
479 unsafe { System.alloc(layout_of(class)) }
480 }
481
482 // SAFETY: the pointer and layout are the ones handed out above, so a
483 // pointer with a pooled class is a block of at least `GRAIN` bytes and can
484 // hold the link. Anything else goes back to the system allocator with the
485 // layout it was made with.
486 #[inline]
487 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
488 let Some(class) = class_of(layout) else {
489 // SAFETY: as for this function; `big_dealloc` hands anything it
490 // does not keep back to the system allocator with this layout.
491 return unsafe { big_dealloc(pointer, layout) };
492 };
493 let kept = HELD
494 .try_with(|held| {
495 let Some(count) = held.get(class) else {
496 return false;
497 };
498 if count.get() >= per_class(class) {
499 return false;
500 }
501 HEADS
502 .try_with(|heads| {
503 let Some(head) = heads.get(class) else {
504 return false;
505 };
506 // SAFETY: the block is at least eight bytes and is not
507 // reachable by anything else once the caller has freed
508 // it, so its first word is ours to use as the link.
509 unsafe { pointer.cast::<*mut u8>().write(head.get()) };
510 head.set(pointer);
511 count.set(count.get().saturating_add(1));
512 true
513 })
514 .unwrap_or(false)
515 })
516 .unwrap_or(false);
517 if kept {
518 return;
519 }
520 // SAFETY: freed with the layout it was allocated with in `alloc`.
521 unsafe { System.dealloc(pointer, layout_of(class)) };
522 }
523
524 // SAFETY: a realloc that stays inside one class is the same block, because
525 // every block in a class is the full class size. Anything else goes through
526 // the default alloc-copy-free, which is what `GlobalAlloc` does when this
527 // is not overridden.
528 #[inline]
529 unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
530 if same_block(layout, new_size) {
531 return pointer;
532 }
533 // SAFETY: the default behaviour, spelled out: a fresh block, the old
534 // bytes copied into it, and the old block freed. Every argument is the
535 // caller's or derived from it.
536 unsafe {
537 let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
538 return std::ptr::null_mut();
539 };
540 let fresh = self.alloc(wanted);
541 if !fresh.is_null() {
542 std::ptr::copy_nonoverlapping(pointer, fresh, layout.size().min(new_size));
543 self.dealloc(pointer, layout);
544 }
545 fresh
546 }
547 }
548}
549
550/// How many bytes [`Carved`] takes from the system at a time for one class.
551const CHUNK_BYTES: usize = 64 << 10;
552
553thread_local! {
554 /// [`Carved`]'s free list heads, one per class.
555 static CARVED_HEADS: [Cell<*mut u8>; CLASSES] =
556 const { [const { Cell::new(std::ptr::null_mut()) }; CLASSES] };
557 /// Where [`Carved`] carves the next block of each class, and where that
558 /// chunk ends.
559 static CARVED_SPANS: [Cell<(usize, usize)>; CLASSES] =
560 const { [const { Cell::new((0, 0)) }; CLASSES] };
561}
562
563/// A size-classed free list that carves its blocks out of larger chunks, for a
564/// program that runs and ends.
565///
566/// **The blocks a program keeps alive are carved, not asked for one at a
567/// time** (task-2191). [`Pooled`] asks the system for every block its lists do
568/// not hold, and a statement that keeps what it builds, such as an `.import` of
569/// 50,000 rows held for one bulk build, empties every list at once and sends
570/// each value to the Windows heap: the heap was 38% of the import. This takes
571/// 64 KiB from the system for a class whose list is empty and hands out blocks
572/// from it, so a block costs a pointer bump.
573///
574/// **Freed blocks are kept, not given back.** A carved block is part of a chunk
575/// and the system cannot take it alone, so a class keeps every block it has
576/// carved, which is at most the most of that class the program held at once.
577/// That is why the command line and the shell install it and the MCP server,
578/// which runs for as long as its client does, keeps [`Pooled`].
579pub struct Carved;
580
581impl Carved {
582 /// Carves one block of a class, taking a new chunk when the last is spent.
583 ///
584 /// @param class - the size class
585 #[inline]
586 fn carve(class: usize) -> *mut u8 {
587 let size = layout_of(class).size();
588 CARVED_SPANS
589 .try_with(|spans| {
590 let Some(span) = spans.get(class) else {
591 return std::ptr::null_mut();
592 };
593 let (mut next, mut end) = span.get();
594 if next.saturating_add(size) > end || next == 0 {
595 let Ok(chunk) = Layout::from_size_align(CHUNK_BYTES.max(size), GRAIN) else {
596 return std::ptr::null_mut();
597 };
598 // SAFETY: a fresh allocation with a valid, nonzero layout.
599 let base = unsafe { System.alloc(chunk) };
600 if base.is_null() {
601 return std::ptr::null_mut();
602 }
603 next = base as usize;
604 end = next.saturating_add(chunk.size());
605 }
606 span.set((next.saturating_add(size), end));
607 next as *mut u8
608 })
609 .unwrap_or(std::ptr::null_mut())
610 }
611}
612
613// SAFETY: a block of a class is either carved from a chunk this allocator
614// took from `System` with room for it, or one it handed out before and was
615// given back with a layout of the same class. Blocks above `LARGEST` or more
616// aligned than the grain go to `System` unchanged in both directions.
617unsafe impl GlobalAlloc for Carved {
618 #[inline]
619 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
620 let Some(class) = class_of(layout) else {
621 // SAFETY: as for this function; `big_alloc` forwards anything it
622 // does not keep to the system allocator unchanged.
623 return unsafe { big_alloc(layout) };
624 };
625 let taken = CARVED_HEADS
626 .try_with(|heads| {
627 let Some(head) = heads.get(class) else {
628 return std::ptr::null_mut();
629 };
630 let block = head.get();
631 if block.is_null() {
632 return std::ptr::null_mut();
633 }
634 // SAFETY: a block on the list holds the next link in its
635 // first eight bytes, written by `dealloc`.
636 head.set(unsafe { block.cast::<*mut u8>().read() });
637 block
638 })
639 .unwrap_or(std::ptr::null_mut());
640 if !taken.is_null() {
641 return taken;
642 }
643 Carved::carve(class)
644 }
645
646 // SAFETY: the pointer and layout are the ones `alloc` handed out, so a
647 // block of a class holds at least the link written into its first word.
648 #[inline]
649 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
650 let Some(class) = class_of(layout) else {
651 // SAFETY: as for this function; `big_dealloc` hands anything it
652 // does not keep back to the system allocator with this layout.
653 return unsafe { big_dealloc(pointer, layout) };
654 };
655 let _ = CARVED_HEADS.try_with(|heads| {
656 if let Some(head) = heads.get(class) {
657 // SAFETY: the block is at least eight bytes and aligned to
658 // the grain, and nothing else holds it any more.
659 unsafe { pointer.cast::<*mut u8>().write(head.get()) };
660 head.set(pointer);
661 }
662 });
663 }
664
665 // SAFETY: as for `Pooled`: a realloc inside one class keeps the block, and
666 // anything else is a fresh block, a copy and a free.
667 #[inline]
668 unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
669 if same_block(layout, new_size) {
670 return pointer;
671 }
672 // SAFETY: a fresh block, the old bytes copied into it and the old block
673 // freed, as `GlobalAlloc`'s own default does.
674 unsafe {
675 let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
676 return std::ptr::null_mut();
677 };
678 let fresh = self.alloc(wanted);
679 if !fresh.is_null() {
680 std::ptr::copy_nonoverlapping(pointer, fresh, layout.size().min(new_size));
681 self.dealloc(pointer, layout);
682 }
683 fresh
684 }
685 }
686}
687
688/// A size-classed free list shared by every thread, for a library.
689///
690/// [`Pooled`] keeps its lists per thread, which is right for a program that
691/// owns its threads. A library loaded into Python or Node does not: the host
692/// starts and ends threads, and every thread that ends would leave its lists
693/// behind, up to [`CLASSES`] times the per class cap. These lists belong to the
694/// process, so nothing is left behind when a thread ends.
695///
696/// **Why a lock per class is still cheaper than the system heap** (task-2191).
697/// The C library spent about 15% of a single row insert into an indexed table
698/// in the Windows heap. A free list here takes one uncontended compare and
699/// swap to lock and one store to unlock, and a host calls the library from one
700/// thread at a time far more often than from several.
701pub struct Shared;
702
703/// Each class's free list head, shared by every thread.
704static SHARED_HEADS: [std::sync::atomic::AtomicPtr<u8>; CLASSES] =
705 [const { std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()) }; CLASSES];
706/// How many blocks each shared class holds.
707static SHARED_HELD: [std::sync::atomic::AtomicUsize; CLASSES] =
708 [const { std::sync::atomic::AtomicUsize::new(0) }; CLASSES];
709/// One lock per class, held only while a head and its count change.
710static SHARED_LOCKS: [std::sync::atomic::AtomicBool; CLASSES] =
711 [const { std::sync::atomic::AtomicBool::new(false) }; CLASSES];
712
713/// Runs `work` with one class's lock held, and returns what it returned.
714///
715/// A spin rather than an operating system lock, because the work it guards is
716/// three loads and stores and an operating system lock could allocate.
717///
718/// @param class - the size class whose lock to take
719/// @param work - what to do while the lock is held
720#[inline]
721fn with_class_lock<R>(class: usize, work: impl FnOnce() -> R) -> Option<R> {
722 use std::sync::atomic::Ordering;
723 let lock = SHARED_LOCKS.get(class)?;
724 while lock
725 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
726 .is_err()
727 {
728 std::hint::spin_loop();
729 }
730 let result = work();
731 lock.store(false, Ordering::Release);
732 Some(result)
733}
734
735// SAFETY: as for `Pooled`. Every block on a list was obtained from `System`
736// with its class's layout, and the class lock makes the head, the link and the
737// count change together, so no two threads take the same block.
738unsafe impl GlobalAlloc for Shared {
739 // SAFETY: a block taken from a list is one `dealloc` put there, whose first
740 // word is the link it wrote; the class lock is held while it is read.
741 #[inline]
742 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
743 use std::sync::atomic::Ordering;
744 let Some(class) = class_of(layout) else {
745 // SAFETY: as for this function; `big_alloc` forwards anything it
746 // does not keep to the system allocator unchanged.
747 return unsafe { big_alloc(layout) };
748 };
749 let (Some(head), Some(held)) = (SHARED_HEADS.get(class), SHARED_HELD.get(class)) else {
750 // SAFETY: forwarded with the class's layout, freed with it below.
751 return unsafe { System.alloc(layout_of(class)) };
752 };
753 // Read without the lock first: an empty list needs no lock at all.
754 if !head.load(Ordering::Relaxed).is_null() {
755 let taken = with_class_lock(class, || {
756 let block = head.load(Ordering::Relaxed);
757 if block.is_null() {
758 return block;
759 }
760 // SAFETY: the block is on the list, so its first word is a link.
761 let next = unsafe { block.cast::<*mut u8>().read() };
762 head.store(next, Ordering::Relaxed);
763 held.store(
764 held.load(Ordering::Relaxed).saturating_sub(1),
765 Ordering::Relaxed,
766 );
767 block
768 })
769 .unwrap_or(std::ptr::null_mut());
770 if !taken.is_null() {
771 return taken;
772 }
773 }
774 // SAFETY: a fresh block of the class's own layout, freed with it below.
775 unsafe { System.alloc(layout_of(class)) }
776 }
777
778 // SAFETY: the pointer and layout are the ones handed out above, so a block
779 // of a pooled class holds at least the link.
780 #[inline]
781 unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
782 use std::sync::atomic::Ordering;
783 let Some(class) = class_of(layout) else {
784 // SAFETY: as for this function; `big_dealloc` hands anything it
785 // does not keep back to the system allocator with this layout.
786 return unsafe { big_dealloc(pointer, layout) };
787 };
788 let (Some(head), Some(held)) = (SHARED_HEADS.get(class), SHARED_HELD.get(class)) else {
789 // SAFETY: freed with the layout it was allocated with.
790 return unsafe { System.dealloc(pointer, layout_of(class)) };
791 };
792 let kept = with_class_lock(class, || {
793 let count = held.load(Ordering::Relaxed);
794 if count >= per_class(class) {
795 return false;
796 }
797 // SAFETY: the caller has freed the block, so its first word is ours.
798 unsafe {
799 pointer
800 .cast::<*mut u8>()
801 .write(head.load(Ordering::Relaxed))
802 };
803 head.store(pointer, Ordering::Relaxed);
804 held.store(count.saturating_add(1), Ordering::Relaxed);
805 true
806 })
807 .unwrap_or(false);
808 if !kept {
809 // SAFETY: freed with the layout it was allocated with in `alloc`.
810 unsafe { System.dealloc(pointer, layout_of(class)) };
811 }
812 }
813
814 // SAFETY: as for `Pooled`: a realloc inside one class keeps the block, and
815 // anything else is a fresh block, a copy and a free.
816 #[inline]
817 unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
818 if same_block(layout, new_size) {
819 return pointer;
820 }
821 if neither_kept(layout, new_size) {
822 // SAFETY: neither size is pooled, so the system allocator made the
823 // block and can grow it in place.
824 return unsafe { System.realloc(pointer, layout, new_size) };
825 }
826 // SAFETY: a fresh block, the old bytes copied, and the old block freed.
827 unsafe {
828 let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
829 return std::ptr::null_mut();
830 };
831 let fresh = self.alloc(wanted);
832 if !fresh.is_null() {
833 std::ptr::copy_nonoverlapping(pointer, fresh, layout.size().min(new_size));
834 self.dealloc(pointer, layout);
835 }
836 fresh
837 }
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 /// The classes cover what they claim to and refuse what they do not.
846 #[test]
847 fn the_classes_are_the_ones_documented() {
848 assert_eq!(class_of(Layout::from_size_align(8, 8).unwrap()), Some(1));
849 assert_eq!(class_of(Layout::from_size_align(16, 16).unwrap()), Some(1));
850 assert_eq!(class_of(Layout::from_size_align(17, 8).unwrap()), Some(2));
851 assert_eq!(
852 class_of(Layout::from_size_align(LARGEST, 16).unwrap()),
853 Some(CLASSES - 1)
854 );
855 // Smaller than the link, which the class's sixteen byte block holds.
856 assert_eq!(class_of(Layout::from_size_align(4, 4).unwrap()), Some(1));
857 assert_eq!(class_of(Layout::from_size_align(1, 1).unwrap()), Some(1));
858 // Too large, too aligned, and empty.
859 assert_eq!(
860 class_of(Layout::from_size_align(LARGEST + 1, 8).unwrap()),
861 None
862 );
863 assert_eq!(class_of(Layout::from_size_align(32, 32).unwrap()), None);
864 assert_eq!(class_of(Layout::from_size_align(0, 1).unwrap()), None);
865 }
866
867 /// Every class's block is at least as large and as aligned as any request
868 /// in it, which is the whole of why reuse is safe.
869 #[test]
870 fn a_class_block_covers_every_request_in_it() {
871 for size in 1..=LARGEST {
872 let Some(layout) = Layout::from_size_align(size, 8).ok() else {
873 continue;
874 };
875 let Some(class) = class_of(layout) else {
876 continue;
877 };
878 let block = layout_of(class);
879 assert!(block.size() >= layout.size(), "size {size}");
880 assert!(block.align() >= layout.align(), "size {size}");
881 }
882 }
883
884 /// A block goes onto its class's list and comes back off it.
885 ///
886 /// Driven through the allocator itself rather than through the lists,
887 /// because what has to hold is that a pointer handed back is one that was
888 /// handed out - the intrusive link is an implementation detail and asserting
889 /// on it would pin the implementation rather than the behaviour.
890 #[test]
891 fn a_freed_block_is_the_one_handed_back() {
892 let layout = Layout::from_size_align(64, 8).expect("a layout");
893 // SAFETY: every pointer here comes from this allocator and is freed
894 // with the layout it was allocated with.
895 unsafe {
896 let first = Pooled.alloc(layout);
897 assert!(!first.is_null());
898 Pooled.dealloc(first, layout);
899 let second = Pooled.alloc(layout);
900 assert_eq!(first, second, "the freed block was not recycled");
901 Pooled.dealloc(second, layout);
902 }
903 }
904
905 /// A block that is written to and recycled does not carry its old bytes
906 /// into a caller's hands as anything but uninitialised memory.
907 ///
908 /// The link is written over the first word of a freed block, so a caller
909 /// that assumed a fresh allocation was zeroed would be reading it. Nothing
910 /// may assume that of `alloc` - `alloc_zeroed` is the one that promises -
911 /// and this pins that the link is confined to the block itself and does not
912 /// run past its end.
913 #[test]
914 fn recycling_stays_inside_the_block() {
915 let layout = Layout::from_size_align(16, 8).expect("a layout");
916 // SAFETY: as above; the guard bytes are a second allocation this test
917 // owns for the length of the check.
918 unsafe {
919 let guard = Pooled.alloc(layout);
920 let block = Pooled.alloc(layout);
921 std::ptr::write_bytes(guard, 0xAB, layout.size());
922 Pooled.dealloc(block, layout);
923 let again = Pooled.alloc(layout);
924 assert_eq!(block, again);
925 for at in 0..layout.size() {
926 assert_eq!(guard.add(at).read(), 0xAB, "the link ran past its block");
927 }
928 Pooled.dealloc(again, layout);
929 Pooled.dealloc(guard, layout);
930 }
931 }
932
933 /// The shared lists recycle a block freed on one thread for a request on
934 /// another, which is what lets a library leave nothing behind when a host
935 /// thread ends.
936 #[test]
937 fn a_shared_block_freed_on_one_thread_serves_another() {
938 // An odd size no other test in this binary asks for, so the class's
939 // list holds only what this test put there.
940 let layout = Layout::from_size_align(3_000, 8).expect("a layout");
941 // SAFETY: the block comes from `Shared` and is freed with its layout.
942 let freed = std::thread::spawn(move || unsafe {
943 let block = Shared.alloc(layout);
944 assert!(!block.is_null());
945 Shared.dealloc(block, layout);
946 block as usize
947 })
948 .join()
949 .expect("the thread ran");
950 // SAFETY: as above.
951 unsafe {
952 let again = Shared.alloc(layout);
953 assert_eq!(
954 again as usize, freed,
955 "the shared list did not recycle the block"
956 );
957 let grown = Shared.realloc(again, layout, 3_001);
958 assert_eq!(grown, again, "a realloc inside one class moved the block");
959 Shared.dealloc(grown, Layout::from_size_align(3_001, 8).expect("a layout"));
960 }
961 }
962
963 /// A carved block is recycled, carved blocks of one chunk do not overlap,
964 /// and a class carves a second chunk when the first is spent.
965 ///
966 /// Driven on a thread of its own so its lists hold only what it put there.
967 #[test]
968 fn carved_blocks_are_distinct_and_recycled() {
969 std::thread::spawn(|| {
970 // An odd size no other test asks for.
971 let layout = Layout::from_size_align(1_000, 8).expect("a layout");
972 let block = layout_of(class_of(layout).expect("a class")).size();
973 let per_chunk = CHUNK_BYTES / block;
974 // SAFETY: every pointer comes from `Carved` and is freed with the
975 // layout it was made with.
976 unsafe {
977 let mut held = Vec::new();
978 for nth in 0..per_chunk.saturating_add(3) {
979 let pointer = Carved.alloc(layout);
980 assert!(!pointer.is_null());
981 std::ptr::write_bytes(pointer, (nth % 251) as u8, layout.size());
982 held.push(pointer);
983 }
984 // No two blocks share a byte: each still holds what was written
985 // into it, after every later block was written.
986 for (nth, pointer) in held.iter().enumerate() {
987 for at in [0, layout.size() - 1] {
988 assert_eq!(pointer.add(at).read(), (nth % 251) as u8, "block {nth}");
989 }
990 }
991 let last = held.pop().expect("a block");
992 Carved.dealloc(last, layout);
993 let again = Carved.alloc(layout);
994 assert_eq!(again, last, "the freed block was not recycled");
995 held.push(again);
996 for pointer in held {
997 Carved.dealloc(pointer, layout);
998 }
999 }
1000 })
1001 .join()
1002 .expect("the thread ran");
1003 }
1004
1005 /// A request above the small classes is rounded to a page sized class, and
1006 /// one above 64 KiB is not kept.
1007 #[test]
1008 fn page_sized_requests_fall_in_power_of_two_classes() {
1009 let class = |size| big_class_of(Layout::from_size_align(size, 8).unwrap());
1010 assert_eq!(class(LARGEST), None, "the small classes hold this");
1011 assert_eq!(class(LARGEST + 1), Some(0));
1012 assert_eq!(class(8 << 10), Some(0));
1013 assert_eq!(class((8 << 10) + 1), Some(1));
1014 assert_eq!(class(32 << 10), Some(2));
1015 assert_eq!(class(64 << 10), Some(3));
1016 assert_eq!(
1017 class((64 << 10) + 1),
1018 None,
1019 "the system allocator holds this"
1020 );
1021 assert_eq!(
1022 big_class_of(Layout::from_size_align(32 << 10, 64).unwrap()),
1023 None,
1024 "more aligned than the grain"
1025 );
1026 }
1027
1028 /// A page sized block grows inside its class without moving, keeps its
1029 /// bytes when it moves to the next class, and is handed out again after it
1030 /// is freed. A block above 64 KiB is never put on a list.
1031 ///
1032 /// The only test that touches the page sized lists, which every thread
1033 /// shares, so nothing else takes the freed block between the free and the
1034 /// next request.
1035 #[test]
1036 fn page_sized_blocks_are_recycled() {
1037 let small = Layout::from_size_align(20_000, 8).unwrap();
1038 // SAFETY: every pointer comes from the allocator it is freed to, with
1039 // the layout it was made or last grown to.
1040 unsafe {
1041 for allocator in [&Shared as &dyn GlobalAlloc, &Carved, &Pooled] {
1042 let pointer = allocator.alloc(small);
1043 assert!(!pointer.is_null());
1044 std::ptr::write_bytes(pointer, 9, small.size());
1045 let grown = allocator.realloc(pointer, small, 30_000);
1046 assert_eq!(grown, pointer, "30,000 bytes is still the 32 KiB class");
1047 let moved =
1048 allocator.realloc(grown, Layout::from_size_align(30_000, 8).unwrap(), 40_000);
1049 assert!(!moved.is_null());
1050 assert_eq!(moved.add(19_999).read(), 9, "the bytes were not kept");
1051 let at_forty = Layout::from_size_align(40_000, 8).unwrap();
1052 allocator.dealloc(moved, at_forty);
1053 let again = allocator.alloc(Layout::from_size_align(64 << 10, 8).unwrap());
1054 assert_eq!(
1055 again, moved,
1056 "the freed 64 KiB block was not handed out again"
1057 );
1058 allocator.dealloc(again, Layout::from_size_align(64 << 10, 8).unwrap());
1059 }
1060 let held = BIG_HELD
1061 .iter()
1062 .map(|count| count.load(std::sync::atomic::Ordering::Relaxed))
1063 .sum::<usize>();
1064 let huge = Layout::from_size_align(128 << 10, 8).unwrap();
1065 let pointer = Shared.alloc(huge);
1066 assert!(!pointer.is_null());
1067 Shared.dealloc(pointer, huge);
1068 let after = BIG_HELD
1069 .iter()
1070 .map(|count| count.load(std::sync::atomic::Ordering::Relaxed))
1071 .sum::<usize>();
1072 assert_eq!(after, held, "a block above 64 KiB was kept");
1073 }
1074 }
1075
1076 /// A realloc inside one class is the same block, and one that crosses a
1077 /// class keeps the bytes.
1078 #[test]
1079 fn realloc_keeps_the_bytes() {
1080 // SAFETY: every pointer comes from this allocator, and every layout is
1081 // the one the block was made with.
1082 unsafe {
1083 let small = Layout::from_size_align(16, 8).expect("a layout");
1084 let block = Pooled.alloc(small);
1085 std::ptr::write_bytes(block, 0x5A, small.size());
1086 let inside = Pooled.realloc(block, small, 12);
1087 assert_eq!(inside, block, "a realloc inside one class moved the block");
1088 let across = Pooled.realloc(inside, small, 200);
1089 assert!(!across.is_null());
1090 for at in 0..small.size() {
1091 assert_eq!(across.add(at).read(), 0x5A, "realloc lost a byte");
1092 }
1093 Pooled.dealloc(across, Layout::from_size_align(200, 8).expect("a layout"));
1094 }
1095 }
1096}