Skip to main content

kevy_alloc/
lib.rs

1//! `kevy-alloc` — a per-shard, mmap-backed, header-free allocator.
2//!
3//! # Why this exists
4//!
5//! Tiering holds kevy's *logical* memory at its budget, but resident
6//! memory ran 2.24× that bound on ~400 B values and 1.65× on 4 KiB ones.
7//! The cause is not tuning: glibc's `brk` arena only shrinks from the
8//! top, so a freed chunk under a live one is a page the OS never gets
9//! back. `malloc_trim(0)` and `MALLOC_ARENA_MAX=2` were both measured
10//! and both moved it by nothing
11//! (the resident-set fragmentation finding measured both no-ops).
12//!
13//! For the small companies kevy is aimed at, RAM is the budget line, so
14//! that ratio decides how much business fits on the box they already
15//! have.
16//!
17//! # What we get from a narrower contract
18//!
19//! A general-purpose allocator serves C's `free(ptr)`, which carries no
20//! size, so it must store one beside every chunk — and those interleaved
21//! headers are part of why the heap cannot shrink. Rust hands us the
22//! `Layout` on deallocation. **This allocator serves sized deallocation
23//! only, so it stores no headers at all**: a pointer's segment, span and
24//! class are recovered by masking the address (see [`segment`]).
25//!
26//! # Status
27//!
28//! Part of an experiment, not a settled design. Every claim here is
29//! under test, and a premise that measurement kills gets changed rather
30//! than worked around — see the allocator RFC
31//! and ROADMAP rule ⑤. The gate is `bench/allocgate.sh`; the accounting
32//! it checks is fixed by `bench/V5-ACCOUNTING-CONTRACT.md`.
33//!
34//! # Standing on shoulders, and where we step off
35//!
36//! - **mimalloc** — segment/page geometry, and the push-only thread-free
37//!   list that makes cross-thread frees ABA-free by construction.
38//! - **tcmalloc** — graded size classes; see [`class`] for why eight
39//!   subdivisions per octave rather than four.
40//! - **Go runtime** — span ownership, and heap accounting as a
41//!   first-class exported thing rather than a debug aid.
42//! - **jemalloc** — decay-style hysteresis before returning pages.
43//! - **torajs-mmalloc** — a working mmap-backed realisation of all of
44//!   the above, plus two lessons it paid for: a missing per-class cap is
45//!   a SIGSEGV rather than a leak, and a cutover without a fast path
46//!   costs 10–30 ns per allocation.
47//!
48//! The step off: those allocators put a thread cache in front of a
49//! shared heap because they cannot know how threads relate to memory.
50//! kevy pins a shard per core, so the heap *is* thread-local and the
51//! fast path is already atomic-free. See [`heap`].
52//!
53//! # Example
54//!
55//! ```
56//! # use kevy_alloc::Heap;
57//! let mut heap = Heap::new(0);
58//! if let Some(p) = heap.alloc(400, 8) {
59//!     // SAFETY: `p` came from this heap with this size and alignment.
60//!     unsafe { heap.dealloc(p, 400, 8) };
61//! }
62//! let stats = heap.snapshot();
63//! assert!(stats.balanced(), "every mapped byte must be accounted for");
64//! ```
65
66// The `global` feature needs thread-local storage, which is a `std`
67// facility; the core allocator itself is `core`-only.
68#![cfg_attr(all(not(test), not(feature = "global")), no_std)]
69#![forbid(unsafe_op_in_unsafe_fn)]
70#![warn(missing_docs)]
71
72pub mod class;
73#[cfg(feature = "global")]
74pub mod global;
75pub mod heap;
76pub mod large;
77pub mod os;
78mod outbound;
79pub mod pagemap;
80mod partials;
81mod reclaim;
82pub mod segment;
83mod snapshot;
84pub mod stats;
85
86#[cfg(feature = "global")]
87pub use global::{KevyAlloc, thread_reclaim, thread_stats};
88pub use heap::{EMPTY_SPAN_HYSTERESIS, Heap, PER_CLASS_CAP};
89pub use large::large_stats;
90pub use stats::Stats;
91
92#[cfg(test)]
93mod tests;