Skip to main content

kevy_alloc/
segment.rs

1//! Segments and spans — where a pointer's identity comes from.
2//!
3//! A **segment** is a 4 MiB region mapped at a 4 MiB-aligned address. It
4//! is cut into 64 **spans** of 64 KiB; span 0 holds the segment header
5//! and the other 63 serve allocations, one size class each.
6//!
7//! That geometry is the whole reason there are no per-allocation
8//! headers. Masking a pointer with `!(SEGMENT_BYTES - 1)` gives the
9//! segment, the offset gives the span index, and the span's metadata
10//! gives the class — so `dealloc` recovers everything it needs from the
11//! address itself. glibc has to store a size beside every chunk because
12//! C's `free` is not told one; we are, and even when we were not, the
13//! address would answer.
14//!
15//! Reference: mimalloc's segment/page split (`segment.c`), and Go's
16//! `mheap` arena indexing. The divergence from both is that a segment
17//! here is owned by exactly one shard for its whole life — kevy pins a
18//! shard per core, so ownership never has to be negotiated.
19
20use core::ptr::NonNull;
21use core::sync::atomic::{AtomicPtr, Ordering};
22
23use crate::class::{self, SPAN_BYTES};
24pub use crate::pagemap::{NO_CLASS, SpanMeta};
25
26/// Bytes per segment. Power of two: the mask is the lookup.
27pub const SEGMENT_BYTES: usize = 4 * 1024 * 1024;
28
29/// Spans in a segment, including the header span.
30pub const SPANS_PER_SEGMENT: usize = SEGMENT_BYTES / SPAN_BYTES;
31
32/// Span index 0 is the header; allocation spans start at 1.
33pub const FIRST_DATA_SPAN: usize = 1;
34
35/// Identifies a live segment header. A pointer that masks to something
36/// without this word is not ours, and that is a bug in the caller
37/// rather than something to paper over.
38const MAGIC: u64 = 0x6b65_7679_616c_6c63; // "kevyallc"
39
40/// The header at the base of every segment.
41#[repr(C)]
42pub struct Segment {
43    magic: u64,
44    /// Intrusive list of a heap's segments — an allocator cannot use a
45    /// `Vec` to track its own memory without recursing into itself.
46    pub next: *mut Segment,
47    /// The shard that owns every span here. Foreign frees find their
48    /// way home through this.
49    pub owner: usize,
50    /// Slots freed by a thread other than the owner, as a lock-free
51    /// stack of slot addresses. See [`push_foreign`] for why this is
52    /// push-only.
53    pub foreign: AtomicPtr<u8>,
54    /// Slot bytes parked on `foreign`, so the accounting can price the
55    /// list without walking it. Bytes rather than a count: one list
56    /// carries slots of several classes, so a count cannot be converted
57    /// back.
58    ///
59    /// `AtomicUsize` rather than `AtomicU64` because 32-bit targets
60    /// (Cortex-M among them) have no 64-bit atomic, and a pending
61    /// foreign-free list cannot exceed the address space anyway.
62    pub foreign_bytes: core::sync::atomic::AtomicUsize,
63    /// Of those, the bytes callers actually asked for.
64    ///
65    /// The owner's `live`/`rounding` counters still include everything on
66    /// this list, because the thread that freed it cannot touch another
67    /// thread's counters. Snapshots move the amount across so it is
68    /// counted once — see `Heap::snapshot`.
69    pub foreign_live: core::sync::atomic::AtomicUsize,
70    /// Per-span bookkeeping, indexed by span number. Index 0 describes
71    /// the header span itself and is never assigned a class.
72    pub spans: [SpanMeta; SPANS_PER_SEGMENT],
73}
74
75impl Segment {
76    /// Initialise a freshly mapped segment in place.
77    ///
78    /// # Safety
79    /// `base` must be a live, writable, 4 MiB-aligned mapping of
80    /// [`SEGMENT_BYTES`] bytes that nothing else references.
81    pub unsafe fn init(base: NonNull<u8>, owner: usize) -> NonNull<Segment> {
82        let seg = base.as_ptr().cast::<Segment>();
83        // SAFETY: the caller guarantees an exclusive writable mapping
84        // large enough for the header, which lives in span 0.
85        unsafe {
86            seg.write(Segment {
87                magic: MAGIC,
88                next: core::ptr::null_mut(),
89                owner,
90                foreign: AtomicPtr::new(core::ptr::null_mut()),
91                foreign_bytes: core::sync::atomic::AtomicUsize::new(0),
92                foreign_live: core::sync::atomic::AtomicUsize::new(0),
93                spans: [SpanMeta::new(); SPANS_PER_SEGMENT],
94            });
95        }
96        // SAFETY: just written.
97        unsafe { NonNull::new_unchecked(seg) }
98    }
99
100    /// Base address of span `index` within this segment.
101    #[must_use]
102    pub fn span_base(&self, index: usize) -> *mut u8 {
103        let base = core::ptr::from_ref(self) as usize;
104        (base + index * SPAN_BYTES) as *mut u8
105    }
106
107    /// Check the header is one of ours. A false result means a pointer
108    /// reached `dealloc` that this allocator never handed out.
109    #[must_use]
110    pub fn is_valid(&self) -> bool {
111        self.magic == MAGIC
112    }
113}
114
115/// Recover the segment owning `ptr`.
116///
117/// # Safety
118/// `ptr` must be a slot address previously handed out by a segment of
119/// this allocator (that is, not from the direct-mapping path).
120#[inline]
121#[must_use]
122pub unsafe fn segment_of(ptr: NonNull<u8>) -> NonNull<Segment> {
123    let base = ptr.as_ptr() as usize & !(SEGMENT_BYTES - 1);
124    // SAFETY: by the caller's contract this address is a live segment
125    // header, since every slot lies inside one.
126    unsafe { NonNull::new_unchecked(base as *mut Segment) }
127}
128
129/// The span index within a segment holding `ptr`.
130#[inline]
131#[must_use]
132pub fn span_index_of(ptr: NonNull<u8>) -> usize {
133    (ptr.as_ptr() as usize & (SEGMENT_BYTES - 1)) / SPAN_BYTES
134}
135
136/// The slot index within its span holding `ptr`, for a given class.
137#[inline]
138#[must_use]
139pub fn slot_index_of(ptr: NonNull<u8>, class: usize) -> u32 {
140    let off = ptr.as_ptr() as usize & (SPAN_BYTES - 1);
141    // A multiply-shift, not a division: this runs on every free (twice
142    // on the claims path), and the owner-thread srcline profile put the
143    // `div` at the top of the post-reorder residue (3.9 %).
144    class::slot_of_offset(off, class)
145}
146
147/// Push a slot onto a segment's foreign-free stack.
148///
149/// # Why this is push-only, and why that matters
150///
151/// A Treiber stack's ABA hazard lives in `pop`: a consumer reads
152/// `head.next`, and between that read and its compare-and-swap another
153/// thread can pop, push other nodes, and push the same address back —
154/// so the CAS succeeds against a stale `next`. torajs-mmalloc documents
155/// the hazard and accepts it, reasoning that its runtime is
156/// single-threaded. kevy is not: values are shared across shards on the
157/// read lane, so a foreign free is ordinary, and inheriting that note
158/// would be inheriting a bug.
159///
160/// The fix is structural rather than defensive. **Only the owning shard
161/// ever removes anything, and it removes the entire list with one
162/// `swap`.** There is no compare-and-swap on the consumer side, so
163/// there is no window for ABA to open. Producers only ever push. This
164/// is mimalloc's thread-free design, and it is strictly simpler than
165/// tagged pointers or hazard pointers would have been.
166///
167/// Splice a pre-linked chain of freed slots onto a segment's foreign
168/// list, and post the batch's byte sums. One CAS and two `fetch_add`s
169/// for the whole chain — this is the amortisation M1 forced: the per-op
170/// version of this function was three atomic RMWs on this same line for
171/// every single foreign free, and cross-shard KV paid 18–39 % for it.
172///
173/// The chain format is unchanged from the per-op era: each slot's first
174/// word links to the next, with the requested size at
175/// [`FOREIGN_SIZE_OFFSET`] — the owner's drain cannot tell a spliced
176/// batch from a thousand individual pushes.
177///
178/// # Safety
179/// `head..tail` must be a chain of live slot addresses belonging to
180/// `seg`, linked through their first words, referenced by nobody else;
181/// `live_sum`/`bytes_sum` must be the chain's requested/slot-byte sums.
182pub unsafe fn splice_foreign(
183    seg: &Segment,
184    head: *mut u8,
185    tail: *mut u8,
186    live_sum: usize,
187    bytes_sum: usize,
188) {
189    seg.foreign_live.fetch_add(live_sum, Ordering::Relaxed);
190    seg.foreign_bytes.fetch_add(bytes_sum, Ordering::Relaxed);
191    let mut old = seg.foreign.load(Ordering::Relaxed);
192    loop {
193        // SAFETY: the tail is ours until the CAS below publishes the
194        // chain; its link word is free to point at the current head.
195        unsafe { tail.cast::<*mut u8>().write(old) };
196        match seg.foreign.compare_exchange_weak(
197            old,
198            head,
199            Ordering::Release,
200            Ordering::Relaxed,
201        ) {
202            Ok(_) => break,
203            Err(actual) => old = actual,
204        }
205    }
206}
207
208/// Take the whole foreign-free list, leaving it empty. Only the owning
209/// shard may call this — that exclusivity is what makes the structure
210/// ABA-free (see [`push_foreign`]).
211#[must_use]
212pub fn take_foreign(seg: &Segment) -> *mut u8 {
213    seg.foreign_bytes.store(0, Ordering::Relaxed);
214    seg.foreign_live.store(0, Ordering::Relaxed);
215    seg.foreign.swap(core::ptr::null_mut(), Ordering::Acquire)
216}
217
218/// Where [`push_foreign`] stores the requested size inside a free slot,
219/// clear of the link that occupies the first word.
220pub const FOREIGN_SIZE_OFFSET: usize = core::mem::size_of::<*mut u8>();
221
222/// Read back the requested size a foreign free recorded.
223///
224/// # Safety
225/// `slot` must still be on a foreign list, untouched since
226/// [`push_foreign`] wrote it.
227#[must_use]
228pub unsafe fn foreign_requested(slot: NonNull<u8>) -> usize {
229    // SAFETY: written by `push_foreign`, and nothing hands out a slot
230    // while it is queued.
231    unsafe { slot.as_ptr().add(FOREIGN_SIZE_OFFSET).cast::<u32>().read() as usize }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn the_header_fits_inside_the_span_it_occupies() {
240        assert!(
241            core::mem::size_of::<Segment>() <= SPAN_BYTES,
242            "the header spills out of span 0 into an allocation span"
243        );
244    }
245
246    #[test]
247    fn geometry_is_maskable() {
248        assert!(SEGMENT_BYTES.is_power_of_two());
249        assert!(SPAN_BYTES.is_power_of_two());
250        assert_eq!(SEGMENT_BYTES % SPAN_BYTES, 0);
251        assert_eq!(SPANS_PER_SEGMENT, 64);
252    }
253
254    #[test]
255    fn the_bitmap_header_still_fits_its_span() {
256        // v2 made SpanMeta deliberately large — the bitmap is the price
257        // of page-granular reclaim, and the header span exists to be
258        // spent on exactly this. The bound that matters is the span.
259        assert!(core::mem::size_of::<SpanMeta>() >= crate::pagemap::BITMAP_WORDS * 8);
260        assert!(core::mem::size_of::<Segment>() <= SPAN_BYTES);
261    }
262}