Skip to main content

moirai_core/pool/
stack.rs

1//! Generation-tagged dual-freelist stack — the single authoritative
2//! slot-freelist implementation in this crate.
3//!
4//! [`crate::memory::MemoryPool`] and [`super::global::GlobalPool`] parameterize
5//! this type instead of duplicating the packed-state algebra.
6//!
7//! `super::slab::SlabAllocator` is intentionally *not* unified here: it uses a
8//! single free list plus a per-slot `occupied` flag so that entries can be
9//! removed by index (random-access deallocation), whereas this stack only
10//! supports LIFO pop from the occupied list. The ABA-protection algebra
11//! (generation counter packed beside the index) is analogous but the list
12//! discipline differs materially.
13
14use crate::platform::*;
15
16/// Sentinel index terminating a freelist chain.
17const SENTINEL: u32 = u32::MAX;
18
19/// Head-of-list word: slot index plus an ABA-protection generation counter,
20/// packed into one `AtomicU64`-storable value.
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22struct PackedState {
23    index: u32,
24    generation: u32,
25}
26
27impl PackedState {
28    #[inline]
29    // justification: intentional bit-unpacking. `val` packs `index` in the low
30    // 32 bits and `generation` in the high 32; each `as u32` extracts one half.
31    // Truncation is the defined semantics (inverse of `to_u64`).
32    #[allow(clippy::cast_possible_truncation)]
33    fn from_u64(val: u64) -> Self {
34        Self {
35            index: val as u32,
36            generation: (val >> 32) as u32,
37        }
38    }
39
40    #[inline]
41    fn to_u64(self) -> u64 {
42        u64::from(self.index) | (u64::from(self.generation) << 32)
43    }
44}
45
46struct StackNode<T> {
47    data: UnsafeCell<MaybeUninit<T>>,
48    next: AtomicU32,
49}
50
51// Safety: StackNode is Send and Sync because access is controlled by LockFreeStack
52unsafe impl<T: Send> Send for StackNode<T> {}
53unsafe impl<T: Sync> Sync for StackNode<T> {}
54
55/// Lock-free stack for object pooling.
56///
57/// # Safety
58/// This implementation uses a pre-allocated array of slots with a generation counter
59/// packed in an `AtomicU64` to prevent ABA problems and use-after-free without blocking.
60///
61/// # Capacity
62/// The slot array is fixed at construction; [`Self::push`] returns the item back
63/// when every slot is occupied. Use [`Self::with_capacity`] to size the stack;
64/// [`Self::new`] uses [`DEFAULT_STACK_CAPACITY`].
65///
66/// # Performance Characteristics
67/// - Push: O(1) amortized, < 20ns
68/// - Pop: O(1) amortized, < 30ns
69/// - Thread-safe: All operations are lock-free
70pub struct LockFreeStack<T> {
71    nodes: Box<[StackNode<T>]>,
72    occupied_head: AtomicU64,
73    free_head: AtomicU64,
74    len: AtomicUsize,
75}
76
77/// Default slot count for [`LockFreeStack::new`].
78///
79/// Sized so a default stack of pointer-sized items costs ~16 KiB of slot
80/// metadata rather than eagerly materializing tens of thousands of nodes;
81/// callers with known workloads size explicitly via
82/// [`LockFreeStack::with_capacity`].
83pub const DEFAULT_STACK_CAPACITY: usize = 1024;
84
85impl<T> LockFreeStack<T> {
86    /// Create a new empty lock-free stack with [`DEFAULT_STACK_CAPACITY`] slots.
87    #[must_use]
88    pub fn new() -> Self {
89        Self::with_capacity(DEFAULT_STACK_CAPACITY)
90    }
91
92    /// Create a new empty lock-free stack with exactly `capacity` slots.
93    ///
94    /// A `capacity` of 0 yields a stack whose `push` always returns the item back.
95    ///
96    /// # Panics
97    /// Panics if `capacity >= u32::MAX` (the sentinel index must stay unused).
98    #[must_use]
99    pub fn with_capacity(capacity: usize) -> Self {
100        assert!(
101            capacity < SENTINEL as usize,
102            "LockFreeStack capacity must be < u32::MAX"
103        );
104        let mut nodes = Vec::with_capacity(capacity);
105        for i in 0..capacity {
106            nodes.push(StackNode {
107                data: UnsafeCell::new(MaybeUninit::uninit()),
108                next: AtomicU32::new(
109                    u32::try_from(i).expect("invariant: i < capacity < u32::MAX (asserted above)")
110                        + 1,
111                ),
112            });
113        }
114        if capacity > 0 {
115            nodes[capacity - 1].next.store(SENTINEL, Ordering::Relaxed);
116        }
117
118        Self {
119            nodes: nodes.into_boxed_slice(),
120            occupied_head: AtomicU64::new(
121                PackedState {
122                    index: SENTINEL,
123                    generation: 0,
124                }
125                .to_u64(),
126            ),
127            free_head: AtomicU64::new(
128                PackedState {
129                    index: if capacity > 0 { 0 } else { SENTINEL },
130                    generation: 0,
131                }
132                .to_u64(),
133            ),
134            len: AtomicUsize::new(0),
135        }
136    }
137
138    #[inline]
139    fn push_list(&self, head: &AtomicU64, index: u32) {
140        loop {
141            let current_val = head.load(Ordering::Acquire);
142            let state = PackedState::from_u64(current_val);
143
144            self.nodes[index as usize]
145                .next
146                .store(state.index, Ordering::Release);
147
148            let new_state = PackedState {
149                index,
150                generation: state.generation.wrapping_add(1),
151            };
152
153            if head
154                .compare_exchange_weak(
155                    current_val,
156                    new_state.to_u64(),
157                    Ordering::Release,
158                    Ordering::Relaxed,
159                )
160                .is_ok()
161            {
162                break;
163            }
164        }
165    }
166
167    #[inline]
168    fn pop_list(&self, head: &AtomicU64) -> Option<u32> {
169        loop {
170            let current_val = head.load(Ordering::Acquire);
171            let state = PackedState::from_u64(current_val);
172            if state.index == SENTINEL {
173                return None;
174            }
175
176            let next = self.nodes[state.index as usize]
177                .next
178                .load(Ordering::Acquire);
179
180            let new_state = PackedState {
181                index: next,
182                generation: state.generation.wrapping_add(1),
183            };
184
185            if head
186                .compare_exchange_weak(
187                    current_val,
188                    new_state.to_u64(),
189                    Ordering::Release,
190                    Ordering::Relaxed,
191                )
192                .is_ok()
193            {
194                return Some(state.index);
195            }
196        }
197    }
198
199    /// Push an item onto the stack.
200    ///
201    /// # Errors
202    /// Returns `Err(item)` — handing the value back to the caller — when every
203    /// slot is occupied. The item is never silently dropped.
204    pub fn push(&self, item: T) -> core::result::Result<(), T> {
205        if let Some(index) = self.pop_list(&self.free_head) {
206            // SAFETY: `index` was exclusively acquired from the free list, so no
207            // other thread reads or writes this slot until it is published to
208            // the occupied list below.
209            unsafe {
210                (*self.nodes[index as usize].data.get()).write(item);
211            }
212            self.push_list(&self.occupied_head, index);
213            self.len.fetch_add(1, Ordering::Release);
214            Ok(())
215        } else {
216            Err(item)
217        }
218    }
219
220    /// Pop an item from the stack.
221    pub fn pop(&self) -> Option<T> {
222        if let Some(index) = self.pop_list(&self.occupied_head) {
223            self.len.fetch_sub(1, Ordering::Release);
224            // SAFETY: `index` was exclusively acquired from the occupied list;
225            // the slot was initialized by the `push` that published it there.
226            let item = unsafe { (*self.nodes[index as usize].data.get()).assume_init_read() };
227            self.push_list(&self.free_head, index);
228            Some(item)
229        } else {
230            None
231        }
232    }
233
234    /// Get the current length of the stack.
235    pub fn len(&self) -> usize {
236        self.len.load(Ordering::Acquire)
237    }
238
239    /// Check if the stack is empty.
240    pub fn is_empty(&self) -> bool {
241        self.len() == 0
242    }
243
244    /// Get the fixed slot capacity of the stack.
245    pub fn capacity(&self) -> usize {
246        self.nodes.len()
247    }
248}
249
250impl<T> Default for LockFreeStack<T> {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256impl<T> Drop for LockFreeStack<T> {
257    fn drop(&mut self) {
258        while self.pop().is_some() {}
259    }
260}
261
262// SAFETY: values change hands through the atomic LIFO head, so `T` must be
263// `Send`; no references to stored values escape the stack.
264unsafe impl<T: Send> Send for LockFreeStack<T> {}
265// SAFETY: shared access runs through lock-free CAS on the head pointer;
266// popped values are owned, never aliased, so `T: Send` suffices.
267unsafe impl<T: Send> Sync for LockFreeStack<T> {}
268
269pub use moirai_utils::cache::CacheAligned;