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;