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}