poolshark/
lib.rs

1//! A high-performance object pool that reuses allocations instead of freeing them.
2//!
3//! # Quick Start
4//!
5//! ```
6//! use poolshark::local::LPooled;
7//! use std::collections::HashMap;
8//!
9//! // Take a HashMap from the thread-local pool (or create new if empty)
10//! let mut map: LPooled<HashMap<String, i32>> = LPooled::take();
11//! map.insert("answer".to_string(), 42);
12//! // When dropped, the HashMap is cleared and returned to the pool
13//! ```
14//!
15//! # Which Pool Should I Use?
16//!
17//! - **Use [`local::LPooled`]** (default choice): Faster, for objects created and dropped on the same thread(s)
18//! - **Use [`global::GPooled`]**: When one thread creates objects and other threads drop them (producer-consumer)
19//!
20//! # Why Poolshark?
21//!
22//! - **Reduce allocations**: Reuse containers instead of repeatedly allocating and freeing
23//! - **Predictable performance**: Consistent behavior across platforms, independent of allocator
24//! - **Fast**: Local pools avoid atomic operations and are more ergonomic than `thread_local!`
25//! - **Flexible**: Choose between fast thread-local pools or lock-free cross-thread pools
26//!
27//! # Pool Types
28//!
29//! ## Global Pooling
30//!
31//! Global pools share objects between threads (see [`global::GPooled`]).
32//! An object taken from a global pool always returns to the pool it was
33//! taken from, regardless of which thread drops it. Use this for producer-consumer
34//! patterns where one thread creates objects and other threads consume them.
35//!
36//! There are several different ways to use global pools. You can use
37//! [take](global::take) or [take_any](global::take_any) to just take objects
38//! from thread local global pools. If you need better performance you can use
39//! [pool](global::pool) or [pool_any](global::pool_any) and then store the pool
40//! somewhere. If you don't have anywhere to store the pool you can use a static
41//! [LazyLock](std::sync::LazyLock) for a truly global named pool. For example,
42//!
43//! ```no_run
44//! use std::{sync::LazyLock, collections::HashMap};
45//! use poolshark::global::{Pool, GPooled};
46//!
47//! type Widget = HashMap<usize, usize>;
48//!
49//! // create a global static widget pool that will accept up to 1024 widgets with
50//! // up to 64 elements of capacity each
51//! static WIDGETS: LazyLock<Pool<Widget>> = LazyLock::new(|| Pool::new(1024, 64));
52//!
53//! fn widget_maker() -> GPooled<Widget> {
54//!     let mut w = WIDGETS.take();
55//!     w.insert(42, 42);
56//!     w
57//! }
58//!
59//! fn widget_user(w: GPooled<Widget>) {
60//!     drop(w) // puts the widget back in the WIDGETS pool
61//! }
62//! ```
63//!
64//! ## Local Pooling
65//!
66//! Local pools (see [`local::LPooled`]) always return dropped objects to a thread-local
67//! pool on whichever thread drops them. They are significantly faster than global pools
68//! because they avoid atomic operations. Use local pools by default unless you have
69//! a cross-thread producer-consumer pattern.
70//!
71//! **Thread safety**: `LPooled<T>` is `Send + Sync` whenever `T` is `Send + Sync`, so you can
72//! safely pass pooled objects between threads.
73//!
74//! Local pools require types to implement the unsafe trait [`IsoPoolable`], but all
75//! standard containers (Vec, HashMap, String, etc.) already implement it.
76//!
77//! ```no_run
78//! use poolshark::local::LPooled;
79//! use std::collections::HashMap;
80//!
81//! type Widget = HashMap<usize, usize>;
82//!
83//! fn widget_maker() -> LPooled<Widget> {
84//!     let mut w = LPooled::<Widget>::default(); // takes from the local pool
85//!     w.insert(42, 42);
86//!     w
87//! }
88//!
89//! fn widget_user(w: LPooled<Widget>) {
90//!     drop(w) // puts the widget back in the local pool
91//! }
92//! ```
93use global::WeakPool;
94pub use poolshark_derive::location_id;
95use std::alloc::Layout;
96
97pub mod global;
98pub mod local;
99pub mod pooled;
100
101/// A globally unique id for a source code position
102///
103/// use poolshark_derive::location_id!() macro to generate one
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct LocationId(pub u16);
106
107#[cfg(test)]
108mod test;
109
110// msb 0 -> it's a layout
111// msb 1 -> it's a size
112//
113// layout: 1 bit flag, 12 bit size, 3 bit align
114//
115// aligns
116// 0x0 -> 1
117// 0x1 -> 2
118// 0x2 -> 4
119// 0x3 -> 8
120// 0x4 -> 16
121//
122// size: 1 bit flag, 15 bit size
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124struct ULayout(u16);
125
126impl Default for ULayout {
127    fn default() -> Self {
128        Self(0)
129    }
130}
131
132impl ULayout {
133    const fn empty() -> Self {
134        Self(0)
135    }
136
137    const fn is_empty(&self) -> bool {
138        self.0 == 0
139    }
140
141    const fn new_layout<T>() -> Option<Self> {
142        let l = Layout::new::<T>();
143        let size = l.size();
144        let align = l.align();
145        if size > 0x0FFF {
146            return None;
147        }
148        let align = match align {
149            1 => 0x0,
150            2 => 0x1,
151            4 => 0x2,
152            8 => 0x3,
153            16 => 0x4,
154            _ => return None,
155        };
156        Some(Self(((size << 3) | align) as u16))
157    }
158
159    const fn new_size(sz: usize) -> Option<Self> {
160        if sz > 0x7FFF {
161            None
162        } else {
163            Some(Self((0x8000 | sz) as u16))
164        }
165    }
166}
167
168macro_rules! add_param {
169    ($d:expr, $p:ty) => {
170        match $d.add_param::<$p>() {
171            Some(d) => d,
172            None => return None,
173        }
174    };
175}
176
177/// Type describing the layout, alignment, and type of a container
178///
179/// `Discriminant` is central to the safety and performance of local pooling. It
180/// describes 2 things in just 8 bytes.
181///
182/// - The unique location in the source code of the implementation of
183/// [IsoPoolable]. This is accomplished by a proc macro that generates a global
184/// table of unique location ids for cross crate source code locations. This
185/// unique id ensures that different container types can't be mixed in the same
186/// pool.
187///
188/// - The layout and alignment of all the type parameters of the
189/// container. Discriminant has 3 slots that can be filled with either
190/// type parameters or const SIZE parameters. If your container has
191/// more parameters than that then you can't locally pool it, and you
192/// can't implement [IsoPoolable]. If you try you will likely cause
193/// undefined behavior.
194///
195/// In order to squeeze all this information into just 8 bytes there are some
196/// limitations.
197///
198/// - You can't have more than 0xFFFF implementations of [IsoPoolable] in the
199/// same project. This includes all the crates depended on by the project.
200///
201/// - Your type parameters must have size <= 0x0FFF bytes and
202///   alignment of 1, 2, 4, 8, or 16. Alignments > 16 will be rejected.
203///
204/// - const SIZE parameters must be <= 0x7FFF.
205///
206/// If any of these constraints are violated the `Discriminant` constructors
207/// will return `None`. If you desire you may panic at that point to cause a
208/// compile error. If you do not panic and instead leave `DISCRIMINANT` as
209/// `None` then local pool operations on that type will work just fine, but
210/// nothing will be pooled. Objects will be freed when they are dropped and
211/// [take](local::take) will allocate new objects each time it is called.
212///
213/// # Discriminant Collisions and Why They're Safe
214///
215/// Two different types can have the same discriminant if they have the same size and
216/// alignment. For example:
217///
218/// ```ignore
219/// #[repr(C)]
220/// struct Padded1 { a: u8, _pad: [u8; 7], b: u64 }  // size 16, align 8
221///
222/// #[repr(C)]
223/// struct Padded2 { x: u64, y: u64 }                 // size 16, align 8
224/// ```
225///
226/// If you pool `Vec<Padded1>` and `Vec<Padded2>`, they would get the same discriminant
227/// because `Padded1` and `Padded2` have identical size and alignment. This means a
228/// `Vec<Padded1>` allocation could be reused as a `Vec<Padded2>` allocation.
229///
230/// **This is safe** because:
231///
232/// 1. Containers are **always empty** when returned to pools (`reset()` ensures this)
233/// 2. An empty `Vec<T>` only cares about `T`'s size and alignment for its allocation
234/// 3. The actual bit patterns inside `T` don't matter when the Vec is empty
235/// 4. When you take from the pool and populate it with your type, it's initialized correctly
236///
237/// The discriminant system is designed to ensure that different container **types** never
238/// share pools (via the `LocationId`), and that the **memory layout** of type parameters
239/// is compatible. As long as containers are properly emptied before pooling (which `reset()`
240/// guarantees), the system is memory safe even with discriminant collisions.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242pub struct Discriminant {
243    container: LocationId,
244    elements: [ULayout; 3],
245}
246
247impl Discriminant {
248    /// return a new empty discriminant
249    pub const fn empty(id: LocationId) -> Discriminant {
250        Discriminant { container: id, elements: [ULayout::empty(); 3] }
251    }
252
253    /// build a discriminant for a type with no type variables (just a location
254    /// id). Always returns Some
255    pub const fn new(id: LocationId) -> Option<Discriminant> {
256        Some(Self::empty(id))
257    }
258
259    /// Add a type parameter.
260    ///
261    /// Discriminant has 3 slots. Each slot can hold either a type
262    /// parameter or a const SIZE. This will return None if the
263    /// discriminant is full, or the type parameter's size or
264    /// alignment are too big.
265    pub const fn add_param<T>(mut self) -> Option<Self> {
266        let l = match ULayout::new_layout::<T>() {
267            None => return None,
268            Some(l) => l,
269        };
270        let mut i = 0;
271        while i < 3 {
272            if self.elements[i].is_empty() {
273                self.elements[i] = l;
274                return Some(self);
275            }
276            i += 1
277        }
278        None
279    }
280
281    /// Add a const SIZE
282    ///
283    /// Discriminant has 3 slots. Each slot can hold either a type
284    /// parameter or a const SIZE. This will return None if the
285    /// discriminant is full, or if the size is too large.
286    pub const fn add_size<const SIZE: usize>(mut self) -> Option<Self> {
287        let l = match ULayout::new_size(SIZE) {
288            None => return None,
289            Some(l) => l,
290        };
291        let mut i = 0;
292        while i < 3 {
293            if self.elements[i].is_empty() {
294                self.elements[i] = l;
295                return Some(self);
296            }
297            i += 1
298        }
299        None
300    }
301
302    /// build a discriminant with one type param
303    pub const fn new_p1<T>(id: LocationId) -> Option<Discriminant> {
304        let d = Discriminant::empty(id);
305        d.add_param::<T>()
306    }
307
308    /// build a discriminant with one type param and a size
309    pub const fn new_p1_size<T, const SIZE: usize>(
310        id: LocationId,
311    ) -> Option<Discriminant> {
312        let d = Discriminant::empty(id);
313        let d = add_param!(d, T);
314        d.add_size::<SIZE>()
315    }
316
317    /// build a discriminant with two type params
318    pub const fn new_p2<T, U>(id: LocationId) -> Option<Discriminant> {
319        let d = Discriminant::empty(id);
320        let d = add_param!(d, T);
321        d.add_param::<U>()
322    }
323
324    /// build a discriminant with two type params and a size
325    pub const fn new_p2_size<T, U, const SIZE: usize>(
326        id: LocationId,
327    ) -> Option<Discriminant> {
328        let d = Discriminant::empty(id);
329        let d = add_param!(d, T);
330        let d = add_param!(d, U);
331        d.add_size::<SIZE>()
332    }
333
334    /// build a discriminant with three type params
335    pub const fn new_p3<T, U, V>(id: LocationId) -> Option<Discriminant> {
336        let d = Discriminant::empty(id);
337        let d = add_param!(d, T);
338        let d = add_param!(d, U);
339        d.add_param::<V>()
340    }
341}
342
343struct Opaque {
344    t: *mut (),
345    drop: Option<Box<dyn FnOnce(*mut ())>>,
346}
347
348impl Drop for Opaque {
349    fn drop(&mut self) {
350        if let Some(f) = self.drop.take() {
351            f(self.t)
352        }
353    }
354}
355
356/// Trait for poolable objects
357pub trait Poolable {
358    /// allocate a new empty collection
359    fn empty() -> Self;
360
361    /// empty the collection and reset it to its default state so it
362    /// can be put back in the pool.
363    fn reset(&mut self);
364
365    /// return the capacity of the collection
366    fn capacity(&self) -> usize;
367
368    /// return true if the object has really been dropped, e.g. if
369    /// you're pooling an Arc then Arc::get_mut().is_some() == true.
370    fn really_dropped(&mut self) -> bool {
371        true
372    }
373}
374
375/// Low level global pool trait for maximum control
376///
377/// Implementing this trait allows full low level control over where the pool
378/// pointer is stored. For example if you are pooling an allocated data
379/// structure, you could store the pool pointer in the allocation to keep the
380/// size of the handle struct to a minimum. E.G. you're pooling a
381/// [triomphe::ThinArc]. Or, if you have a static global pool, then you would
382/// not need to keep a pool pointer at all.
383///
384/// The object's drop implementation should return the object to the
385/// pool instead of deallocating it
386///
387/// Implementing this trait correctly is extremely tricky, and requires unsafe
388/// code, therefore it is marked as unsafe.
389///
390/// Most of the time you should use the [GPooled](global::GPooled) wrapper.
391pub unsafe trait RawPoolable: Sized {
392    /// allocate a new empty object and set it's pool pointer to `pool`
393    fn empty(pool: WeakPool<Self>) -> Self;
394
395    /// empty the collection and reset it to its default state so it
396    /// can be put back in the pool
397    fn reset(&mut self);
398
399    /// return the capacity of the collection
400    fn capacity(&self) -> usize;
401
402    /// Actually drop the inner object, don't put it back in the pool,
403    /// make sure you do not call both this method and the drop
404    /// implementation that puts the object back in the pool!
405    fn really_drop(self);
406}
407
408/// Trait for isomorphicly poolable objects.
409///
410/// That is objects that can safely be pooled by memory layout and container
411/// type. For example two `HashMap`s, `HashMap<usize, usize>` and
412/// `HashMap<ArcStr, ArcStr>` are isomorphic, their memory allocations can be
413/// used interchangably so long as they are empty.
414pub unsafe trait IsoPoolable: Poolable {
415    /// # Getting the Layout Right
416    ///
417    /// You must pass every type variable that can effect the layout
418    /// of the container's inner allocation to Discriminant. Take
419    /// HashMap as an example. If you build the discriminant such as
420    /// `Discriminant::new_p1::<HashMap<K, V>>()` it would always be
421    /// the same for any `K`, `V`, because the `HashMap` struct
422    /// doesn't actually contain any `K`s or `V`s, just a pointer to
423    /// some `K`s and `V`s. If you implemented discriminant this way
424    /// it would cause undefined behavior when you tried to pool two
425    /// HashMap's with `K`, `V` types that aren't isomorphic. Instead
426    /// you must pass `K` and `V` to `Discriminant::new_p2::<K, V>()`
427    /// to get the real layout of the inner collection of
428    /// `HashMap`. This is why this trait is unsafe to implement, if
429    /// you aren't careful when you build the discriminant very bad
430    /// things will happen.
431    ///
432    /// # Why not TypeId
433    ///
434    /// The reason why Discriminant is used instead of
435    /// [`TypeId`](std::any::TypeId) (which would accomplish the same
436    /// goal) is twofold. First Discriminant is 1 word on a 64 bit
437    /// machine, and thus very fast to index, and second `TypeId` only
438    /// supports types without references. However we often want to
439    /// pool empty containers where the inner type is a reference,
440    /// thus we cannot use `TypeId`.
441    ///
442    /// # Why Discriminant is an Option
443    ///
444    /// Discriminant is a compressed version of layout that squeezes 2
445    /// layouts a size and a container type into 8 bytes. As such
446    /// there are some layouts that are too big to fit in it, and the
447    /// constructor will return None in those cases. For the purpose
448    /// of pooling containers of small objects these tradeoffs seemed
449    /// worth it. If you must pool containers of huge objects like
450    /// this, you can use the global pools.
451    ///
452    /// # Arc
453    ///
454    /// It is not safe to implement this trait for
455    /// [`Arc`](std::sync::Arc) or in general for any container that
456    /// can't be totally empty. This is because having the same
457    /// Discriminant only guarantees that two types are isomorphic, it
458    /// does not guarantee that they have the same bit patterns.
459    /// Normal container types are safe in spite of this because reset
460    /// makes sure they are empty, and thus no errent bit patterns
461    /// exist in the container and all we care about is that the
462    /// container's allocation is isomorphic with respect to the types
463    /// we want to put in it. However `Arc` can never be empty, and
464    /// since notch optimization may change the bit pattern of `None`
465    /// depending on the type of `T`, it is not even safe to pool
466    /// `Arc<Option<T>>`. Because if `T` and `U` were isomorphic, but
467    /// notch optimization used a different bit pattern for `None`,
468    /// then pooling these objects could cause undefined behavior.
469    const DISCRIMINANT: Option<Discriminant>;
470}