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}