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}