Skip to main content

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}