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