Skip to main content

cubecl_common/
pool.rs

1//! A dynamically-growing pool of single-cell items handed out as exclusive, non-cloneable handles.
2
3use crate::stub::{AtomicBool, Ordering};
4use alloc::boxed::Box;
5use alloc::vec::Vec;
6use core::cell::UnsafeCell;
7
8/// Resets a pooled value so the pool can hand it out again.
9pub trait Reclaim {
10    /// Clear the value in place, keeping its allocation for the next acquire.
11    fn reclaim(&mut self);
12}
13
14/// A pooled value that stays owned by the [`LeasePool`] even while checked out.
15///
16/// `leased` is the free/used flag; the value lives in an `UnsafeCell` so a handout can mutate it in
17/// place while the pool still holds a shared reference to the item.
18struct LeaseSlot<T: Default + Reclaim> {
19    leased: AtomicBool,
20    item: UnsafeCell<T>,
21}
22
23impl<T: Default + Reclaim> Default for LeaseSlot<T> {
24    fn default() -> Self {
25        Self {
26            leased: AtomicBool::new(false),
27            item: UnsafeCell::new(T::default()),
28        }
29    }
30}
31
32// SAFETY: `leased` guarantees at most one live `LeaseHandle` references an item at a
33// time, so writes through the `UnsafeCell` are never aliased by another writer, and the pool only
34// ever forms shared references to the item (to read/write `leased`).
35unsafe impl<T: Default + Reclaim> Send for LeaseSlot<T> {}
36unsafe impl<T: Default + Reclaim> Sync for LeaseSlot<T> {}
37
38/// Pool of reusable values with single-cell items that are not cloneable.
39///
40/// Items stay in the pool while checked out; [`Self::acquire`] hands back a
41/// [`LeaseHandle`] pointing at one and flags it `leased`. Dropping the handle reclaims the
42/// value and frees the slot, so allocations are reused across acquires instead of allocating anew.
43#[derive(Default)]
44pub struct LeasePool<T: Default + Reclaim> {
45    /// Boxed so each item keeps a stable address when the `Vec` grows — handles hold raw pointers
46    /// into these allocations, which must stay valid across `push`.
47    items: Vec<Box<LeaseSlot<T>>>,
48}
49
50impl<T: Default + Reclaim> core::fmt::Debug for LeasePool<T> {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        f.debug_struct("LeasePool")
53            .field("items", &self.items.len())
54            .finish()
55    }
56}
57
58impl<T: Default + Reclaim> LeasePool<T> {
59    /// Pre-reserve room for `capacity` pooled items (if exceeded, the pool still grows on demand).
60    pub fn with_capacity(capacity: usize) -> Self {
61        Self {
62            items: Vec::with_capacity(capacity),
63        }
64    }
65
66    /// Take an item, reusing a free pooled item when available.
67    pub fn acquire(&mut self) -> LeaseHandle<T> {
68        let item: &LeaseSlot<T> = match self.items.iter().find(|item| {
69            item.leased
70                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
71                .is_ok()
72        }) {
73            Some(item) => item,
74            None => {
75                let item = Box::new(LeaseSlot::default());
76                item.leased.store(true, Ordering::Release);
77                self.items.push(item);
78                self.items.last().unwrap()
79            }
80        };
81
82        LeaseHandle { item }
83    }
84}
85
86/// Handle to a pooled item. Reclaims the value and frees its slot when dropped.
87///
88/// The pointee is owned by the [`LeasePool`], which never removes items, so it outlives
89/// every handle. An atomic `leased` flag keeps the reference unique for the handle's lifetime.
90pub struct LeaseHandle<T: Default + Reclaim> {
91    item: *const LeaseSlot<T>,
92}
93
94// SAFETY: the handle may be moved across threads by its holder. The pointee is `Send + Sync` and,
95// per the `leased` flag invariant, uniquely owned by this handle.
96unsafe impl<T: Default + Reclaim> Send for LeaseHandle<T> {}
97
98impl<T: Default + Reclaim> core::ops::Deref for LeaseHandle<T> {
99    type Target = T;
100
101    fn deref(&self) -> &Self::Target {
102        // SAFETY: unique access is guaranteed by the `leased` flag; the pointee outlives the handle.
103        unsafe { &*(*self.item).item.get() }
104    }
105}
106
107impl<T: Default + Reclaim> core::ops::DerefMut for LeaseHandle<T> {
108    fn deref_mut(&mut self) -> &mut Self::Target {
109        // SAFETY: see `deref`; `&mut self` guarantees no other reference through this handle.
110        unsafe { &mut *(*self.item).item.get() }
111    }
112}
113
114impl<T: Default + Reclaim> core::fmt::Debug for LeaseHandle<T> {
115    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116        f.debug_struct("LeaseHandle").finish_non_exhaustive()
117    }
118}
119
120impl<T: Default + Reclaim> Drop for LeaseHandle<T> {
121    fn drop(&mut self) {
122        // SAFETY: the pool outlives the handle, so the pointee is still valid.
123        let item = unsafe { &*self.item };
124        // Reclaim any value not drained by the holder (e.g. a caller that failed mid-use), keeping
125        // the allocation for the next acquire.
126        unsafe { &mut *item.item.get() }.reclaim();
127        item.leased.store(false, Ordering::Release);
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use alloc::{vec, vec::Vec};
135
136    /// Stand-in pooled value: a `Vec` so we can observe capacity reuse and reclaim clearing.
137    #[derive(Default)]
138    struct Probe(Vec<u32>);
139
140    impl Reclaim for Probe {
141        fn reclaim(&mut self) {
142            self.0.clear();
143        }
144    }
145
146    impl<T: Default + Reclaim> LeasePool<T> {
147        fn len(&self) -> usize {
148            self.items.len()
149        }
150    }
151
152    /// The key soundness case: acquiring more items grows `items`, which must not invalidate
153    /// handles already handed out (they hold raw pointers into the boxed slots). Run under Miri
154    /// to check the aliasing model, not just the observable values.
155    #[test]
156    fn growth_keeps_live_handles_valid() {
157        // Start empty so every acquire pushes a new item, reallocating `items` while earlier
158        // handles are still live.
159        let mut pool = LeasePool::<Probe>::with_capacity(0);
160
161        let mut a = pool.acquire();
162        a.0.push(1);
163        let mut b = pool.acquire();
164        b.0.push(2);
165        let mut c = pool.acquire();
166        c.0.push(3);
167
168        // Write through the earliest handle again, after two growth-triggering pushes.
169        a.0.push(10);
170
171        assert_eq!(a.0, vec![1, 10]);
172        assert_eq!(b.0, vec![2]);
173        assert_eq!(c.0, vec![3]);
174    }
175
176    /// Dropping a handle reclaims (clears) the value but keeps its allocation for the next acquire.
177    #[test]
178    fn drop_reclaims_and_reuses_allocation() {
179        let mut pool = LeasePool::<Probe>::with_capacity(0);
180
181        let mut a = pool.acquire();
182        a.0.extend([1, 2, 3, 4]);
183        let cap = a.0.capacity();
184        assert!(cap >= 4);
185        drop(a);
186
187        // The only free slot is the one just released; it comes back cleared but with its
188        // allocation intact.
189        let reused = pool.acquire();
190        assert!(reused.0.is_empty());
191        assert_eq!(reused.0.capacity(), cap);
192    }
193
194    /// A released slot is reused on the next acquire rather than growing the pool.
195    #[test]
196    fn frees_slot_for_later_acquire() {
197        let mut pool = LeasePool::<Probe>::with_capacity(0);
198        for _ in 0..8 {
199            let mut handle = pool.acquire();
200            handle.0.push(0);
201        }
202        // Only ever one live handle at a time, so the pool holds exactly one item.
203        assert_eq!(pool.len(), 1);
204    }
205}