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 its small
551/// classes.
552const CHUNK_BYTES: usize = 64 << 10;
553
554/// How many bytes [`Carved`] takes from the system at a time for one page
555/// sized class: sixteen 32 KiB frames.
556///
557/// Reserving it costs one call and no page faults: the memory is touched only
558/// when a block of it is used.
559const BIG_CHUNK_BYTES: usize = 512 << 10;
560
561/// The alignment of a page sized chunk, one operating system page.
562///
563/// A 32 KiB block on a page boundary covers eight pages. The Windows heap's
564/// 16 byte aligned block covers nine, and the ninth is one more first touch
565/// fault on every page the buffer pool reads.
566const BIG_CHUNK_ALIGN: usize = 4 << 10;
567
568thread_local! {
569    /// [`Carved`]'s free list heads, one per class.
570    static CARVED_HEADS: [Cell<*mut u8>; CLASSES] =
571        const { [const { Cell::new(std::ptr::null_mut()) }; CLASSES] };
572    /// Where [`Carved`] carves the next small block, of any class, and where
573    /// that chunk ends.
574    static CARVED_SPAN: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
575    /// [`Carved`]'s free list heads for the page sized classes.
576    static CARVED_BIG_HEADS: [Cell<*mut u8>; BIG_CLASSES] =
577        const { [const { Cell::new(std::ptr::null_mut()) }; BIG_CLASSES] };
578    /// Where [`Carved`] carves the next block of each page sized class.
579    static CARVED_BIG_SPANS: [Cell<(usize, usize)>; BIG_CLASSES] =
580        const { [const { Cell::new((0, 0)) }; BIG_CLASSES] };
581}
582
583/// A size-classed free list that carves its blocks out of larger chunks, for a
584/// program that runs and ends.
585///
586/// **The blocks a program keeps alive are carved, not asked for one at a
587/// time** (task-2191). [`Pooled`] asks the system for every block its lists do
588/// not hold, and a statement that keeps what it builds, such as an `.import` of
589/// 50,000 rows held for one bulk build, empties every list at once and sends
590/// each value to the Windows heap: the heap was 38% of the import. This takes
591/// 64 KiB from the system at a time and hands out blocks from it, so a block
592/// costs a pointer bump.
593///
594/// **Every small class carves from the same chunk.** With a chunk per class, a
595/// one row query, which allocates 3,248 blocks in more than a hundred classes,
596/// touched at least one fresh 4 KiB page per class, and each first touch is a
597/// page fault. From one chunk, blocks of different sizes share pages, and the
598/// pages touched follow the bytes allocated. A class's free list still holds
599/// only its own blocks.
600///
601/// **Freed blocks are kept, not given back.** A carved block is part of a chunk
602/// and the system cannot take it alone, so a class keeps every block it has
603/// carved, which is at most the most of that class the program held at once.
604/// That is why the command line and the shell install it and the MCP server,
605/// which runs for as long as its client does, keeps [`Pooled`].
606///
607/// **Page sized blocks are carved too, on page boundaries.** Every buffer pool
608/// frame is a 32 KiB block, and a cold query from the command line reads tens
609/// of pages into frames used for the first time. Taken from the Windows heap
610/// one at a time, each block cost a heap call and nine first touch faults.
611/// Carved from a 512 KiB chunk aligned to a page, it costs a pointer bump and
612/// eight. In a new process, reading 64 pages of 32 KiB took 681 us into heap
613/// blocks and 473 us into one page aligned region.
614pub struct Carved;
615
616impl Carved {
617    /// Carves one block of a class, taking a new chunk when the last is spent.
618    ///
619    /// The rest of a spent chunk, less than the 4 KiB of the largest small
620    /// class, is left unused.
621    ///
622    /// @param class - the size class
623    #[inline]
624    fn carve(class: usize) -> *mut u8 {
625        let size = layout_of(class).size();
626        CARVED_SPAN
627            .try_with(|span| {
628                let (mut next, mut end) = span.get();
629                if next.saturating_add(size) > end || next == 0 {
630                    let Ok(chunk) = Layout::from_size_align(CHUNK_BYTES.max(size), GRAIN) else {
631                        return std::ptr::null_mut();
632                    };
633                    // SAFETY: a fresh allocation with a valid, nonzero layout.
634                    let base = unsafe { System.alloc(chunk) };
635                    if base.is_null() {
636                        return std::ptr::null_mut();
637                    }
638                    next = base as usize;
639                    end = next.saturating_add(chunk.size());
640                }
641                span.set((next.saturating_add(size), end));
642                next as *mut u8
643            })
644            .unwrap_or(std::ptr::null_mut())
645    }
646
647    /// Allocates a block too large for the small classes: a page sized block,
648    /// or one from the system for anything larger or more aligned.
649    ///
650    /// Kept out of line: `alloc` is inlined into every allocation in the
651    /// program, and with this path inlined too the command line's code grew
652    /// from 7.0 MB to 8.7 MB. A large block is rare next to a small one.
653    ///
654    /// @param layout - the allocation's layout
655    ///
656    /// # Safety
657    ///
658    /// As for [`GlobalAlloc::alloc`].
659    #[inline(never)]
660    unsafe fn alloc_large(layout: Layout) -> *mut u8 {
661        if let Some(big) = big_class_of(layout) {
662            return Carved::alloc_big(big);
663        }
664        // SAFETY: a size above the page sized classes, or an alignment above
665        // the grain, goes to the system unchanged.
666        unsafe { System.alloc(layout) }
667    }
668
669    /// Frees a block [`Carved::alloc_large`] made.
670    ///
671    /// @param pointer - the block
672    /// @param layout - the layout it was allocated with
673    ///
674    /// # Safety
675    ///
676    /// As for [`GlobalAlloc::dealloc`].
677    #[inline(never)]
678    unsafe fn dealloc_large(pointer: *mut u8, layout: Layout) {
679        if let Some(big) = big_class_of(layout) {
680            return Carved::free_big(pointer, big);
681        }
682        // SAFETY: `alloc_large` took this block from the system with this
683        // layout.
684        unsafe { System.dealloc(pointer, layout) }
685    }
686
687    /// Hands out one page sized block: from the class's free list when it
688    /// holds one, or carved from the class's chunk.
689    ///
690    /// When the thread's lists are gone, which happens only while the thread
691    /// is ending, the block comes from the system with the class's own
692    /// layout, so it is still a whole block of its class if it is ever freed
693    /// onto a list.
694    ///
695    /// @param class - the page sized class
696    #[inline]
697    fn alloc_big(class: usize) -> *mut u8 {
698        let taken = CARVED_BIG_HEADS
699            .try_with(|heads| {
700                let Some(head) = heads.get(class) else {
701                    return std::ptr::null_mut();
702                };
703                let block = head.get();
704                if block.is_null() {
705                    return Carved::carve_big(class);
706                }
707                // SAFETY: a block on the list holds the next link in its
708                // first eight bytes, written by `free_big`.
709                head.set(unsafe { block.cast::<*mut u8>().read() });
710                block
711            })
712            .unwrap_or(std::ptr::null_mut());
713        if !taken.is_null() {
714            return taken;
715        }
716        // SAFETY: a fresh allocation with the class's valid, nonzero layout.
717        unsafe { System.alloc(big_layout_of(class)) }
718    }
719
720    /// Carves one page sized block, taking a new page aligned chunk when the
721    /// class's last chunk is spent.
722    ///
723    /// @param class - the page sized class
724    #[inline]
725    fn carve_big(class: usize) -> *mut u8 {
726        let size = big_layout_of(class).size();
727        CARVED_BIG_SPANS
728            .try_with(|spans| {
729                let Some(span) = spans.get(class) else {
730                    return std::ptr::null_mut();
731                };
732                let (mut next, mut end) = span.get();
733                if next.saturating_add(size) > end || next == 0 {
734                    let Ok(chunk) =
735                        Layout::from_size_align(BIG_CHUNK_BYTES.max(size), BIG_CHUNK_ALIGN)
736                    else {
737                        return std::ptr::null_mut();
738                    };
739                    // SAFETY: a fresh allocation with a valid, nonzero layout.
740                    let base = unsafe { System.alloc(chunk) };
741                    if base.is_null() {
742                        return std::ptr::null_mut();
743                    }
744                    next = base as usize;
745                    end = next.saturating_add(chunk.size());
746                }
747                span.set((next.saturating_add(size), end));
748                next as *mut u8
749            })
750            .unwrap_or(std::ptr::null_mut())
751    }
752
753    /// Puts a page sized block on its class's free list. It is never given
754    /// back to the system, because a carved block is part of a chunk.
755    ///
756    /// @param pointer - the block
757    /// @param class - its page sized class
758    #[inline]
759    fn free_big(pointer: *mut u8, class: usize) {
760        let _ = CARVED_BIG_HEADS.try_with(|heads| {
761            if let Some(head) = heads.get(class) {
762                // SAFETY: the block is at least 8 KiB and aligned to the grain,
763                // and nothing else holds it any more.
764                unsafe { pointer.cast::<*mut u8>().write(head.get()) };
765                head.set(pointer);
766            }
767        });
768    }
769}
770
771// SAFETY: a block of a class, small or page sized, is either carved from a
772// chunk this allocator took from `System` with room for it, taken from
773// `System` with the class's own layout, or one it handed out before and was
774// given back with a layout of the same class. Blocks above 64 KiB or more
775// aligned than the grain go to `System` unchanged in both directions.
776unsafe impl GlobalAlloc for Carved {
777    // SAFETY: as the comment on this impl says. Out of line, as it was before
778    // the page sized classes were carved: with them carved, `alloc` and
779    // `dealloc` became small enough to inline at every allocation and every
780    // drop, the command line's code grew from 7.0 MB to 8.7 MB, and a one row
781    // query took 0.3 ms longer to start.
782    #[inline(never)]
783    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
784        let Some(class) = class_of(layout) else {
785            // SAFETY: as for this function.
786            return unsafe { Carved::alloc_large(layout) };
787        };
788        let taken = CARVED_HEADS
789            .try_with(|heads| {
790                let Some(head) = heads.get(class) else {
791                    return std::ptr::null_mut();
792                };
793                let block = head.get();
794                if block.is_null() {
795                    return std::ptr::null_mut();
796                }
797                // SAFETY: a block on the list holds the next link in its
798                // first eight bytes, written by `dealloc`.
799                head.set(unsafe { block.cast::<*mut u8>().read() });
800                block
801            })
802            .unwrap_or(std::ptr::null_mut());
803        if !taken.is_null() {
804            return taken;
805        }
806        Carved::carve(class)
807    }
808
809    // SAFETY: the pointer and layout are the ones `alloc` handed out, so a
810    // block of a class holds at least the link written into its first word.
811    #[inline(never)]
812    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
813        let Some(class) = class_of(layout) else {
814            // SAFETY: as for this function.
815            return unsafe { Carved::dealloc_large(pointer, layout) };
816        };
817        let _ = CARVED_HEADS.try_with(|heads| {
818            if let Some(head) = heads.get(class) {
819                // SAFETY: the block is at least eight bytes and aligned to
820                // the grain, and nothing else holds it any more.
821                unsafe { pointer.cast::<*mut u8>().write(head.get()) };
822                head.set(pointer);
823            }
824        });
825    }
826
827    // SAFETY: as for `Pooled`: a realloc inside one class keeps the block, and
828    // anything else is a fresh block, a copy and a free.
829    #[inline]
830    unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
831        if same_block(layout, new_size) {
832            return pointer;
833        }
834        // SAFETY: a fresh block, the old bytes copied into it and the old block
835        // freed, as `GlobalAlloc`'s own default does.
836        unsafe {
837            let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
838                return std::ptr::null_mut();
839            };
840            let fresh = self.alloc(wanted);
841            if !fresh.is_null() {
842                std::ptr::copy_nonoverlapping(pointer, fresh, layout.size().min(new_size));
843                self.dealloc(pointer, layout);
844            }
845            fresh
846        }
847    }
848}
849
850/// A size-classed free list shared by every thread, for a library.
851///
852/// [`Pooled`] keeps its lists per thread, which is right for a program that
853/// owns its threads. A library loaded into Python or Node does not: the host
854/// starts and ends threads, and every thread that ends would leave its lists
855/// behind, up to [`CLASSES`] times the per class cap. These lists belong to the
856/// process, so nothing is left behind when a thread ends.
857///
858/// **Why a lock per class is still cheaper than the system heap** (task-2191).
859/// The C library spent about 15% of a single row insert into an indexed table
860/// in the Windows heap. A free list here takes one uncontended compare and
861/// swap to lock and one store to unlock, and a host calls the library from one
862/// thread at a time far more often than from several.
863pub struct Shared;
864
865/// Each class's free list head, shared by every thread.
866static SHARED_HEADS: [std::sync::atomic::AtomicPtr<u8>; CLASSES] =
867    [const { std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()) }; CLASSES];
868/// How many blocks each shared class holds.
869static SHARED_HELD: [std::sync::atomic::AtomicUsize; CLASSES] =
870    [const { std::sync::atomic::AtomicUsize::new(0) }; CLASSES];
871/// One lock per class, held only while a head and its count change.
872static SHARED_LOCKS: [std::sync::atomic::AtomicBool; CLASSES] =
873    [const { std::sync::atomic::AtomicBool::new(false) }; CLASSES];
874
875/// Runs `work` with one class's lock held, and returns what it returned.
876///
877/// A spin rather than an operating system lock, because the work it guards is
878/// three loads and stores and an operating system lock could allocate.
879///
880/// @param class - the size class whose lock to take
881/// @param work - what to do while the lock is held
882#[inline]
883fn with_class_lock<R>(class: usize, work: impl FnOnce() -> R) -> Option<R> {
884    use std::sync::atomic::Ordering;
885    let lock = SHARED_LOCKS.get(class)?;
886    while lock
887        .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
888        .is_err()
889    {
890        std::hint::spin_loop();
891    }
892    let result = work();
893    lock.store(false, Ordering::Release);
894    Some(result)
895}
896
897// SAFETY: as for `Pooled`. Every block on a list was obtained from `System`
898// with its class's layout, and the class lock makes the head, the link and the
899// count change together, so no two threads take the same block.
900unsafe impl GlobalAlloc for Shared {
901    // SAFETY: a block taken from a list is one `dealloc` put there, whose first
902    // word is the link it wrote; the class lock is held while it is read.
903    #[inline]
904    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
905        use std::sync::atomic::Ordering;
906        let Some(class) = class_of(layout) else {
907            // SAFETY: as for this function; `big_alloc` forwards anything it
908            // does not keep to the system allocator unchanged.
909            return unsafe { big_alloc(layout) };
910        };
911        let (Some(head), Some(held)) = (SHARED_HEADS.get(class), SHARED_HELD.get(class)) else {
912            // SAFETY: forwarded with the class's layout, freed with it below.
913            return unsafe { System.alloc(layout_of(class)) };
914        };
915        // Read without the lock first: an empty list needs no lock at all.
916        if !head.load(Ordering::Relaxed).is_null() {
917            let taken = with_class_lock(class, || {
918                let block = head.load(Ordering::Relaxed);
919                if block.is_null() {
920                    return block;
921                }
922                // SAFETY: the block is on the list, so its first word is a link.
923                let next = unsafe { block.cast::<*mut u8>().read() };
924                head.store(next, Ordering::Relaxed);
925                held.store(
926                    held.load(Ordering::Relaxed).saturating_sub(1),
927                    Ordering::Relaxed,
928                );
929                block
930            })
931            .unwrap_or(std::ptr::null_mut());
932            if !taken.is_null() {
933                return taken;
934            }
935        }
936        // SAFETY: a fresh block of the class's own layout, freed with it below.
937        unsafe { System.alloc(layout_of(class)) }
938    }
939
940    // SAFETY: the pointer and layout are the ones handed out above, so a block
941    // of a pooled class holds at least the link.
942    #[inline]
943    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
944        use std::sync::atomic::Ordering;
945        let Some(class) = class_of(layout) else {
946            // SAFETY: as for this function; `big_dealloc` hands anything it
947            // does not keep back to the system allocator with this layout.
948            return unsafe { big_dealloc(pointer, layout) };
949        };
950        let (Some(head), Some(held)) = (SHARED_HEADS.get(class), SHARED_HELD.get(class)) else {
951            // SAFETY: freed with the layout it was allocated with.
952            return unsafe { System.dealloc(pointer, layout_of(class)) };
953        };
954        let kept = with_class_lock(class, || {
955            let count = held.load(Ordering::Relaxed);
956            if count >= per_class(class) {
957                return false;
958            }
959            // SAFETY: the caller has freed the block, so its first word is ours.
960            unsafe {
961                pointer
962                    .cast::<*mut u8>()
963                    .write(head.load(Ordering::Relaxed))
964            };
965            head.store(pointer, Ordering::Relaxed);
966            held.store(count.saturating_add(1), Ordering::Relaxed);
967            true
968        })
969        .unwrap_or(false);
970        if !kept {
971            // SAFETY: freed with the layout it was allocated with in `alloc`.
972            unsafe { System.dealloc(pointer, layout_of(class)) };
973        }
974    }
975
976    // SAFETY: as for `Pooled`: a realloc inside one class keeps the block, and
977    // anything else is a fresh block, a copy and a free.
978    #[inline]
979    unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
980        if same_block(layout, new_size) {
981            return pointer;
982        }
983        if neither_kept(layout, new_size) {
984            // SAFETY: neither size is pooled, so the system allocator made the
985            // block and can grow it in place.
986            return unsafe { System.realloc(pointer, layout, new_size) };
987        }
988        // SAFETY: a fresh block, the old bytes copied, and the old block freed.
989        unsafe {
990            let Ok(wanted) = Layout::from_size_align(new_size, layout.align()) else {
991                return std::ptr::null_mut();
992            };
993            let fresh = self.alloc(wanted);
994            if !fresh.is_null() {
995                std::ptr::copy_nonoverlapping(pointer, fresh, layout.size().min(new_size));
996                self.dealloc(pointer, layout);
997            }
998            fresh
999        }
1000    }
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005    use super::*;
1006
1007    /// The classes cover what they claim to and refuse what they do not.
1008    #[test]
1009    fn the_classes_are_the_ones_documented() {
1010        assert_eq!(class_of(Layout::from_size_align(8, 8).unwrap()), Some(1));
1011        assert_eq!(class_of(Layout::from_size_align(16, 16).unwrap()), Some(1));
1012        assert_eq!(class_of(Layout::from_size_align(17, 8).unwrap()), Some(2));
1013        assert_eq!(
1014            class_of(Layout::from_size_align(LARGEST, 16).unwrap()),
1015            Some(CLASSES - 1)
1016        );
1017        // Smaller than the link, which the class's sixteen byte block holds.
1018        assert_eq!(class_of(Layout::from_size_align(4, 4).unwrap()), Some(1));
1019        assert_eq!(class_of(Layout::from_size_align(1, 1).unwrap()), Some(1));
1020        // Too large, too aligned, and empty.
1021        assert_eq!(
1022            class_of(Layout::from_size_align(LARGEST + 1, 8).unwrap()),
1023            None
1024        );
1025        assert_eq!(class_of(Layout::from_size_align(32, 32).unwrap()), None);
1026        assert_eq!(class_of(Layout::from_size_align(0, 1).unwrap()), None);
1027    }
1028
1029    /// Every class's block is at least as large and as aligned as any request
1030    /// in it, which is the whole of why reuse is safe.
1031    #[test]
1032    fn a_class_block_covers_every_request_in_it() {
1033        for size in 1..=LARGEST {
1034            let Some(layout) = Layout::from_size_align(size, 8).ok() else {
1035                continue;
1036            };
1037            let Some(class) = class_of(layout) else {
1038                continue;
1039            };
1040            let block = layout_of(class);
1041            assert!(block.size() >= layout.size(), "size {size}");
1042            assert!(block.align() >= layout.align(), "size {size}");
1043        }
1044    }
1045
1046    /// A block goes onto its class's list and comes back off it.
1047    ///
1048    /// Driven through the allocator itself rather than through the lists,
1049    /// because what has to hold is that a pointer handed back is one that was
1050    /// handed out - the intrusive link is an implementation detail and asserting
1051    /// on it would pin the implementation rather than the behaviour.
1052    #[test]
1053    fn a_freed_block_is_the_one_handed_back() {
1054        let layout = Layout::from_size_align(64, 8).expect("a layout");
1055        // SAFETY: every pointer here comes from this allocator and is freed
1056        // with the layout it was allocated with.
1057        unsafe {
1058            let first = Pooled.alloc(layout);
1059            assert!(!first.is_null());
1060            Pooled.dealloc(first, layout);
1061            let second = Pooled.alloc(layout);
1062            assert_eq!(first, second, "the freed block was not recycled");
1063            Pooled.dealloc(second, layout);
1064        }
1065    }
1066
1067    /// A block that is written to and recycled does not carry its old bytes
1068    /// into a caller's hands as anything but uninitialised memory.
1069    ///
1070    /// The link is written over the first word of a freed block, so a caller
1071    /// that assumed a fresh allocation was zeroed would be reading it. Nothing
1072    /// may assume that of `alloc` - `alloc_zeroed` is the one that promises -
1073    /// and this pins that the link is confined to the block itself and does not
1074    /// run past its end.
1075    #[test]
1076    fn recycling_stays_inside_the_block() {
1077        let layout = Layout::from_size_align(16, 8).expect("a layout");
1078        // SAFETY: as above; the guard bytes are a second allocation this test
1079        // owns for the length of the check.
1080        unsafe {
1081            let guard = Pooled.alloc(layout);
1082            let block = Pooled.alloc(layout);
1083            std::ptr::write_bytes(guard, 0xAB, layout.size());
1084            Pooled.dealloc(block, layout);
1085            let again = Pooled.alloc(layout);
1086            assert_eq!(block, again);
1087            for at in 0..layout.size() {
1088                assert_eq!(guard.add(at).read(), 0xAB, "the link ran past its block");
1089            }
1090            Pooled.dealloc(again, layout);
1091            Pooled.dealloc(guard, layout);
1092        }
1093    }
1094
1095    /// The shared lists recycle a block freed on one thread for a request on
1096    /// another, which is what lets a library leave nothing behind when a host
1097    /// thread ends.
1098    #[test]
1099    fn a_shared_block_freed_on_one_thread_serves_another() {
1100        // An odd size no other test in this binary asks for, so the class's
1101        // list holds only what this test put there.
1102        let layout = Layout::from_size_align(3_000, 8).expect("a layout");
1103        // SAFETY: the block comes from `Shared` and is freed with its layout.
1104        let freed = std::thread::spawn(move || unsafe {
1105            let block = Shared.alloc(layout);
1106            assert!(!block.is_null());
1107            Shared.dealloc(block, layout);
1108            block as usize
1109        })
1110        .join()
1111        .expect("the thread ran");
1112        // SAFETY: as above.
1113        unsafe {
1114            let again = Shared.alloc(layout);
1115            assert_eq!(
1116                again as usize, freed,
1117                "the shared list did not recycle the block"
1118            );
1119            let grown = Shared.realloc(again, layout, 3_001);
1120            assert_eq!(grown, again, "a realloc inside one class moved the block");
1121            Shared.dealloc(grown, Layout::from_size_align(3_001, 8).expect("a layout"));
1122        }
1123    }
1124
1125    /// A carved block is recycled, carved blocks of one chunk do not overlap,
1126    /// and a class carves a second chunk when the first is spent.
1127    ///
1128    /// Driven on a thread of its own so its lists hold only what it put there.
1129    #[test]
1130    fn carved_blocks_are_distinct_and_recycled() {
1131        std::thread::spawn(|| {
1132            // An odd size no other test asks for.
1133            let layout = Layout::from_size_align(1_000, 8).expect("a layout");
1134            let block = layout_of(class_of(layout).expect("a class")).size();
1135            let per_chunk = CHUNK_BYTES / block;
1136            // SAFETY: every pointer comes from `Carved` and is freed with the
1137            // layout it was made with.
1138            unsafe {
1139                let mut held = Vec::new();
1140                for nth in 0..per_chunk.saturating_add(3) {
1141                    let pointer = Carved.alloc(layout);
1142                    assert!(!pointer.is_null());
1143                    std::ptr::write_bytes(pointer, (nth % 251) as u8, layout.size());
1144                    held.push(pointer);
1145                }
1146                // No two blocks share a byte: each still holds what was written
1147                // into it, after every later block was written.
1148                for (nth, pointer) in held.iter().enumerate() {
1149                    for at in [0, layout.size() - 1] {
1150                        assert_eq!(pointer.add(at).read(), (nth % 251) as u8, "block {nth}");
1151                    }
1152                }
1153                let last = held.pop().expect("a block");
1154                Carved.dealloc(last, layout);
1155                let again = Carved.alloc(layout);
1156                assert_eq!(again, last, "the freed block was not recycled");
1157                held.push(again);
1158                for pointer in held {
1159                    Carved.dealloc(pointer, layout);
1160                }
1161            }
1162        })
1163        .join()
1164        .expect("the thread ran");
1165    }
1166
1167    /// `Carved` carves page sized blocks on page boundaries, keeps them apart,
1168    /// hands a freed one out again, and keeps a block's bytes when a
1169    /// reallocation moves it to a larger class.
1170    #[test]
1171    fn carved_page_sized_blocks_are_page_aligned_distinct_and_recycled() {
1172        std::thread::spawn(|| {
1173            let layout = Layout::from_size_align(32 << 10, 8).expect("a layout");
1174            let per_chunk = BIG_CHUNK_BYTES / layout.size();
1175            // SAFETY: every pointer comes from `Carved` and is freed with the
1176            // layout it was made with.
1177            unsafe {
1178                let mut held = Vec::new();
1179                for nth in 0..per_chunk.saturating_add(3) {
1180                    let pointer = Carved.alloc(layout);
1181                    assert!(!pointer.is_null());
1182                    assert_eq!(pointer as usize % BIG_CHUNK_ALIGN, 0, "block {nth}");
1183                    std::ptr::write_bytes(pointer, (nth % 251) as u8, layout.size());
1184                    held.push(pointer);
1185                }
1186                for (nth, pointer) in held.iter().enumerate() {
1187                    for at in [0, layout.size() - 1] {
1188                        assert_eq!(pointer.add(at).read(), (nth % 251) as u8, "block {nth}");
1189                    }
1190                }
1191                let last = held.pop().expect("a block");
1192                Carved.dealloc(last, layout);
1193                let again = Carved.alloc(layout);
1194                assert_eq!(again, last, "the freed block was not recycled");
1195                // Growing to the 64 KiB class moves the block and keeps its bytes.
1196                std::ptr::write_bytes(again, 7, layout.size());
1197                let grown = Carved.realloc(again, layout, 40 << 10);
1198                assert!(!grown.is_null());
1199                assert_eq!(grown.read(), 7);
1200                assert_eq!(grown.add(layout.size() - 1).read(), 7);
1201                Carved.dealloc(
1202                    grown,
1203                    Layout::from_size_align(40 << 10, 8).expect("a layout"),
1204                );
1205                for pointer in held {
1206                    Carved.dealloc(pointer, layout);
1207                }
1208            }
1209        })
1210        .join()
1211        .expect("the thread ran");
1212    }
1213
1214    /// A request above the small classes is rounded to a page sized class, and
1215    /// one above 64 KiB is not kept.
1216    #[test]
1217    fn page_sized_requests_fall_in_power_of_two_classes() {
1218        let class = |size| big_class_of(Layout::from_size_align(size, 8).unwrap());
1219        assert_eq!(class(LARGEST), None, "the small classes hold this");
1220        assert_eq!(class(LARGEST + 1), Some(0));
1221        assert_eq!(class(8 << 10), Some(0));
1222        assert_eq!(class((8 << 10) + 1), Some(1));
1223        assert_eq!(class(32 << 10), Some(2));
1224        assert_eq!(class(64 << 10), Some(3));
1225        assert_eq!(
1226            class((64 << 10) + 1),
1227            None,
1228            "the system allocator holds this"
1229        );
1230        assert_eq!(
1231            big_class_of(Layout::from_size_align(32 << 10, 64).unwrap()),
1232            None,
1233            "more aligned than the grain"
1234        );
1235    }
1236
1237    /// A page sized block grows inside its class without moving, keeps its
1238    /// bytes when it moves to the next class, and is handed out again after it
1239    /// is freed. A block above 64 KiB is never put on a list.
1240    ///
1241    /// The only test that touches the page sized lists, which every thread
1242    /// shares, so nothing else takes the freed block between the free and the
1243    /// next request.
1244    #[test]
1245    fn page_sized_blocks_are_recycled() {
1246        let small = Layout::from_size_align(20_000, 8).unwrap();
1247        // SAFETY: every pointer comes from the allocator it is freed to, with
1248        // the layout it was made or last grown to.
1249        unsafe {
1250            for allocator in [&Shared as &dyn GlobalAlloc, &Carved, &Pooled] {
1251                let pointer = allocator.alloc(small);
1252                assert!(!pointer.is_null());
1253                std::ptr::write_bytes(pointer, 9, small.size());
1254                let grown = allocator.realloc(pointer, small, 30_000);
1255                assert_eq!(grown, pointer, "30,000 bytes is still the 32 KiB class");
1256                let moved =
1257                    allocator.realloc(grown, Layout::from_size_align(30_000, 8).unwrap(), 40_000);
1258                assert!(!moved.is_null());
1259                assert_eq!(moved.add(19_999).read(), 9, "the bytes were not kept");
1260                let at_forty = Layout::from_size_align(40_000, 8).unwrap();
1261                allocator.dealloc(moved, at_forty);
1262                let again = allocator.alloc(Layout::from_size_align(64 << 10, 8).unwrap());
1263                assert_eq!(
1264                    again, moved,
1265                    "the freed 64 KiB block was not handed out again"
1266                );
1267                allocator.dealloc(again, Layout::from_size_align(64 << 10, 8).unwrap());
1268            }
1269            let held = BIG_HELD
1270                .iter()
1271                .map(|count| count.load(std::sync::atomic::Ordering::Relaxed))
1272                .sum::<usize>();
1273            let huge = Layout::from_size_align(128 << 10, 8).unwrap();
1274            let pointer = Shared.alloc(huge);
1275            assert!(!pointer.is_null());
1276            Shared.dealloc(pointer, huge);
1277            let after = BIG_HELD
1278                .iter()
1279                .map(|count| count.load(std::sync::atomic::Ordering::Relaxed))
1280                .sum::<usize>();
1281            assert_eq!(after, held, "a block above 64 KiB was kept");
1282        }
1283    }
1284
1285    /// A realloc inside one class is the same block, and one that crosses a
1286    /// class keeps the bytes.
1287    #[test]
1288    fn realloc_keeps_the_bytes() {
1289        // SAFETY: every pointer comes from this allocator, and every layout is
1290        // the one the block was made with.
1291        unsafe {
1292            let small = Layout::from_size_align(16, 8).expect("a layout");
1293            let block = Pooled.alloc(small);
1294            std::ptr::write_bytes(block, 0x5A, small.size());
1295            let inside = Pooled.realloc(block, small, 12);
1296            assert_eq!(inside, block, "a realloc inside one class moved the block");
1297            let across = Pooled.realloc(inside, small, 200);
1298            assert!(!across.is_null());
1299            for at in 0..small.size() {
1300                assert_eq!(across.add(at).read(), 0x5A, "realloc lost a byte");
1301            }
1302            Pooled.dealloc(across, Layout::from_size_align(200, 8).expect("a layout"));
1303        }
1304    }
1305}