Skip to main content

buddy_slab_allocator/slab/
cache.rs

1/// Per-size-class slab cache.
2///
3/// Maintains three intrusive doubly-linked lists of slab pages:
4/// - **partial**: some objects free (preferred for allocation)
5/// - **full**: no objects free
6/// - **empty**: all objects free (at most one cached; rest returned to buddy)
7use super::page::{SlabListState, SlabPageHeader};
8use super::size_class::SizeClass;
9
10/// Intrusive list head (address of the first `SlabPageHeader`, 0 = empty).
11#[derive(Debug, Clone, Copy)]
12struct ListHead {
13    first: usize,
14    kind: SlabListState,
15}
16
17impl ListHead {
18    const fn empty(kind: SlabListState) -> Self {
19        Self { first: 0, kind }
20    }
21
22    fn is_empty(&self) -> bool {
23        self.first == 0
24    }
25
26    /// Push a slab page onto the front of the list.
27    ///
28    /// # Safety
29    /// `base` must point to a valid `SlabPageHeader`.
30    unsafe fn push_front(&mut self, base: usize) {
31        unsafe {
32            let hdr = &mut *(base as *mut SlabPageHeader);
33            debug_assert_eq!(
34                hdr.list_state,
35                SlabListState::None,
36                "pushing slab onto {:?} while it is on {:?}",
37                self.kind,
38                hdr.list_state
39            );
40            debug_assert_eq!(hdr.list_prev, 0, "pushing slab with stale list_prev");
41            debug_assert_eq!(hdr.list_next, 0, "pushing slab with stale list_next");
42            hdr.list_state = self.kind;
43            hdr.list_prev = 0;
44            hdr.list_next = self.first;
45            if self.first != 0 {
46                let old = &mut *(self.first as *mut SlabPageHeader);
47                debug_assert_eq!(
48                    old.list_state, self.kind,
49                    "list head points to slab with wrong membership"
50                );
51                old.list_prev = base;
52            }
53            self.first = base;
54        }
55    }
56
57    /// Remove a slab page from this list.
58    ///
59    /// # Safety
60    /// `base` must be in this list.
61    unsafe fn remove(&mut self, base: usize) {
62        unsafe {
63            let hdr = &*(base as *const SlabPageHeader);
64            debug_assert_eq!(
65                hdr.list_state, self.kind,
66                "removing slab from {:?} while it is on {:?}",
67                self.kind, hdr.list_state
68            );
69            if hdr.list_prev == 0 {
70                debug_assert_eq!(self.first, base, "front slab is not this list's head");
71            } else {
72                debug_assert_eq!(
73                    (*(hdr.list_prev as *const SlabPageHeader)).list_next,
74                    base,
75                    "previous slab does not link back to removed slab"
76                );
77            }
78            if hdr.list_next != 0 {
79                debug_assert_eq!(
80                    (*(hdr.list_next as *const SlabPageHeader)).list_prev,
81                    base,
82                    "next slab does not link back to removed slab"
83                );
84            }
85            let prev = hdr.list_prev;
86            let next = hdr.list_next;
87
88            if prev != 0 {
89                (*(prev as *mut SlabPageHeader)).list_next = next;
90            } else {
91                self.first = next;
92            }
93            if next != 0 {
94                (*(next as *mut SlabPageHeader)).list_prev = prev;
95            }
96            // Clear links
97            let hdr = &mut *(base as *mut SlabPageHeader);
98            hdr.list_prev = 0;
99            hdr.list_next = 0;
100            hdr.list_state = SlabListState::None;
101        }
102    }
103
104    /// Pop the first page from the list.  Returns 0 if empty.
105    unsafe fn pop_front(&mut self) -> usize {
106        unsafe {
107            if self.first == 0 {
108                return 0;
109            }
110            let base = self.first;
111            self.remove(base);
112            base
113        }
114    }
115}
116
117/// Cache for a single [`SizeClass`].
118pub struct SlabCache {
119    pub size_class: SizeClass,
120    partial: ListHead,
121    full: ListHead,
122    empty: ListHead,
123    /// Number of empty slabs cached (we keep at most 1).
124    empty_count: usize,
125}
126
127/// Result of a per-cache deallocation.
128pub enum CacheDeallocResult {
129    /// Object freed, slab stays.
130    Done,
131    /// Slab became empty and should be returned to the page allocator.
132    FreeSlab { base: usize, pages: usize },
133}
134
135impl SlabCache {
136    pub const fn new(size_class: SizeClass) -> Self {
137        Self {
138            size_class,
139            partial: ListHead::empty(SlabListState::Partial),
140            full: ListHead::empty(SlabListState::Full),
141            empty: ListHead::empty(SlabListState::Empty),
142            empty_count: 0,
143        }
144    }
145
146    /// Try to allocate one object.  Returns `Some(obj_addr)` or `None` if no slabs available.
147    pub fn alloc_object<const PAGE_SIZE: usize>(&mut self) -> Option<usize> {
148        // 1. Try the first partial slab (drain remote frees first).
149        if let Some(addr) = self.try_alloc_from_partial::<PAGE_SIZE>() {
150            return Some(addr);
151        }
152
153        // 2. A full slab may have gained free objects via lock-free remote frees.
154        if let Some(base) = self.reclaim_full_with_remote_frees() {
155            unsafe { self.partial.push_front(base) };
156            return self.try_alloc_from_partial::<PAGE_SIZE>();
157        }
158
159        // 3. Try recycling an empty slab.
160        if !self.empty.is_empty() {
161            let base = unsafe { self.empty.pop_front() };
162            self.empty_count -= 1;
163            // Move to partial and alloc from it.
164            unsafe { self.partial.push_front(base) };
165            return self.try_alloc_from_partial::<PAGE_SIZE>();
166        }
167
168        None
169    }
170
171    /// Drain remote frees from the first full slab that has them and move it
172    /// back to the partial list.
173    fn reclaim_full_with_remote_frees(&mut self) -> Option<usize> {
174        let mut base = self.full.first;
175        while base != 0 {
176            let next = unsafe { (*(base as *const SlabPageHeader)).list_next };
177            let hdr = unsafe { &mut *(base as *mut SlabPageHeader) };
178            if hdr.has_remote_frees() {
179                hdr.drain_remote_frees(base);
180                unsafe { self.full.remove(base) };
181                return Some(base);
182            }
183            base = next;
184        }
185        None
186    }
187
188    /// Attempt allocation from the first partial slab.
189    fn try_alloc_from_partial<const PAGE_SIZE: usize>(&mut self) -> Option<usize> {
190        let base = self.partial.first;
191        if base == 0 {
192            return None;
193        }
194
195        let hdr = unsafe { &mut *(base as *mut SlabPageHeader) };
196
197        // Drain any remote frees first.
198        if hdr.has_remote_frees() {
199            hdr.drain_remote_frees(base);
200        }
201
202        if let Some(idx) = hdr.local_alloc() {
203            let obj_addr = hdr.object_addr(base, idx);
204            // If slab is now full, move to full list.
205            if hdr.is_local_full() && !hdr.has_remote_frees() {
206                unsafe {
207                    self.partial.remove(base);
208                    self.full.push_front(base);
209                }
210            }
211            return Some(obj_addr);
212        }
213        None
214    }
215
216    /// Free an object back to this cache (local CPU path — under lock).
217    ///
218    /// Returns whether the slab should be returned to the page allocator.
219    pub fn dealloc_object<const PAGE_SIZE: usize>(
220        &mut self,
221        obj_addr: usize,
222    ) -> CacheDeallocResult {
223        let slab_bytes = self.size_class.slab_pages(PAGE_SIZE) * PAGE_SIZE;
224        let base = SlabPageHeader::base_from_obj_addr::<PAGE_SIZE>(obj_addr, slab_bytes);
225        let hdr = unsafe { &mut *(base as *mut SlabPageHeader) };
226        let was_full = hdr.list_state == SlabListState::Full;
227
228        let idx = hdr.object_index(base, obj_addr);
229        hdr.local_free(idx);
230
231        if was_full {
232            // Move from full to partial.
233            unsafe {
234                self.full.remove(base);
235                self.partial.push_front(base);
236            }
237        }
238
239        // Check if slab is now completely empty.
240        // First drain remote frees so we have an accurate count.
241        if hdr.has_remote_frees() {
242            hdr.drain_remote_frees(base);
243        }
244
245        if hdr.is_all_free() {
246            if self.empty_count == 0 {
247                // Cache one empty slab for reuse.
248                unsafe {
249                    self.partial.remove(base);
250                    self.empty.push_front(base);
251                }
252                self.empty_count += 1;
253                CacheDeallocResult::Done
254            } else {
255                // Already have a cached empty slab — return this one.
256                unsafe { self.partial.remove(base) };
257                let hdr = unsafe { &mut *(base as *mut SlabPageHeader) };
258                hdr.prepare_for_buddy_free();
259                CacheDeallocResult::FreeSlab {
260                    base,
261                    pages: self.size_class.slab_pages(PAGE_SIZE),
262                }
263            }
264        } else {
265            CacheDeallocResult::Done
266        }
267    }
268
269    /// Register a newly allocated slab page (from the buddy allocator).
270    pub fn add_slab(&mut self, base: usize, bytes: usize, owner_cpu: u16) {
271        let hdr = unsafe { &mut *(base as *mut SlabPageHeader) };
272        hdr.init(self.size_class, bytes, owner_cpu);
273        unsafe { self.partial.push_front(base) };
274    }
275}