Skip to main content

subms_arena_allocator/
lib.rs

1//! Fixed-capacity bump-pointer arena. Allocate fixed-layout values into a
2//! single pre-sized byte buffer; `reset()` rewinds the bump cursor so the
3//! next request can reuse the entire arena.
4//!
5//! **Drop is NOT run on items in the arena.** Anything with a non-trivial
6//! `Drop` (e.g. `String`, `Vec`) will leak its heap allocation if you store
7//! it in this arena. [`Bump::alloc_copy`] is constrained to `Copy` types as
8//! the safe public surface.
9//!
10//! Base is single-chunk and fixed-capacity. When the chunk is exhausted
11//! `alloc_*` panics and `try_alloc_*` returns `None`. Opt into the
12//! `growable` feature for the auto-grow variant.
13//!
14//! **Thread safety: none, by construction.** The crate declares no
15//! `unsafe impl Send` / `Sync`. The raw-pointer arenas ([`Bump`],
16//! `GrowableBump`, `AlignedBump`) are therefore neither `Send` nor
17//! `Sync`; `TypedArena<T>` owns plain `Vec` storage so it inherits `T`'s
18//! auto traits, and every mutating method takes `&mut self`, so a shared
19//! reference cannot allocate. Give each thread its own arena rather than
20//! reaching for a lock - a shared cursor is a contended cache line,
21//! which is the cost this structure exists to avoid.
22//!
23//! ```
24//! use subms_arena_allocator::Bump;
25//! let mut a = Bump::with_capacity(1024);
26//! let x: &mut u32 = a.alloc_copy(42u32);
27//! assert_eq!(*x, 42);
28//! a.reset();
29//! ```
30//!
31//! Full writeup, design notes and measured benchmarks:
32//! <https://www.submillisecond.com/cookbook/recipes/subms-arena-allocator>
33
34use std::alloc::{Layout, alloc, dealloc};
35use std::ptr;
36
37/// Fixed-capacity bump-pointer arena.
38pub struct Bump {
39    ptr: *mut u8,
40    layout: Layout,
41    cursor: usize,
42}
43
44impl Bump {
45    /// New empty arena with a 4 KiB chunk.
46    pub fn new() -> Self {
47        Self::with_capacity(4096)
48    }
49
50    /// New arena, pre-allocating a single chunk of `capacity` bytes
51    /// (promoted to a 64-byte floor and 16-byte alignment).
52    pub fn with_capacity(capacity: usize) -> Self {
53        let capacity = capacity.max(64);
54        let layout = Layout::from_size_align(capacity, 16).expect("layout");
55        let ptr = unsafe { alloc(layout) };
56        assert!(!ptr.is_null(), "OOM allocating arena chunk");
57        Self {
58            ptr,
59            layout,
60            cursor: 0,
61        }
62    }
63
64    /// Allocate a `Copy` value. Panics if the arena is out of room.
65    pub fn alloc_copy<T: Copy>(&mut self, value: T) -> &mut T {
66        let cursor = self.cursor;
67        let cap = self.layout.size();
68        match self.try_alloc_copy(value) {
69            Some(r) => r,
70            None => panic!(
71                "Bump out of capacity: cursor={} layout_size={} requested={}",
72                cursor,
73                cap,
74                std::mem::size_of::<T>(),
75            ),
76        }
77    }
78
79    /// Fallible alloc. Returns `None` if the arena can't fit the value
80    /// at its natural alignment.
81    pub fn try_alloc_copy<T: Copy>(&mut self, value: T) -> Option<&mut T> {
82        let layout = Layout::new::<T>();
83        let p = self.try_alloc_raw(layout)?;
84        unsafe {
85            ptr::write(p as *mut T, value);
86            Some(&mut *(p as *mut T))
87        }
88    }
89
90    /// Allocate `layout.size()` bytes aligned to `layout.align()`.
91    /// Panics if the request doesn't fit.
92    pub fn alloc_raw(&mut self, layout: Layout) -> *mut u8 {
93        let cursor = self.cursor;
94        let cap = self.layout.size();
95        let requested = layout.size();
96        match self.try_alloc_raw(layout) {
97            Some(p) => p,
98            None => panic!(
99                "Bump out of capacity: cursor={cursor} layout_size={cap} requested={requested}",
100            ),
101        }
102    }
103
104    /// Fallible raw alloc. Returns `None` if the request doesn't fit.
105    pub fn try_alloc_raw(&mut self, layout: Layout) -> Option<*mut u8> {
106        let size = layout.size();
107        let align = layout.align();
108        let base = self.ptr as usize;
109        let aligned_abs = align_up(base + self.cursor, align);
110        let aligned = aligned_abs - base;
111        let end = aligned.checked_add(size)?;
112        if end > self.layout.size() {
113            return None;
114        }
115        self.cursor = end;
116        Some(unsafe { self.ptr.add(aligned) })
117    }
118
119    /// Rewind to empty. The buffer is retained for reuse.
120    pub fn reset(&mut self) {
121        self.cursor = 0;
122    }
123
124    /// Bytes used so far in the current chunk.
125    pub fn used(&self) -> usize {
126        self.cursor
127    }
128
129    /// Total capacity of the single backing chunk.
130    pub fn capacity(&self) -> usize {
131        self.layout.size()
132    }
133
134    /// Backwards-compatible alias for [`Bump::capacity`].
135    pub fn total_capacity(&self) -> usize {
136        self.capacity()
137    }
138}
139
140impl Default for Bump {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl Drop for Bump {
147    fn drop(&mut self) {
148        unsafe { dealloc(self.ptr, self.layout) };
149    }
150}
151
152/// `(p + align - 1) & !(align - 1)` - rounds up to the next aligned address.
153#[inline]
154pub(crate) fn align_up(p: usize, align: usize) -> usize {
155    debug_assert!(align.is_power_of_two(), "alignment must be a power of two");
156    (p + align - 1) & !(align - 1)
157}
158
159#[cfg(feature = "harness")]
160pub mod recipe;
161
162#[cfg(any(
163    feature = "typed",
164    feature = "growable",
165    feature = "stats",
166    feature = "aligned",
167))]
168pub mod features;
169
170#[cfg(feature = "aligned")]
171pub use features::aligned::AlignedBump;
172#[cfg(feature = "growable")]
173pub use features::growable::GrowableBump;
174#[cfg(feature = "stats")]
175pub use features::stats::{BumpStats, StatsBump};
176#[cfg(feature = "typed")]
177pub use features::typed::{Slot, TypedArena};
178
179#[cfg(test)]
180#[path = "arena_tests.rs"]
181mod arena_tests;
182
183#[cfg(test)]
184#[path = "sample_app_tests.rs"]
185mod sample_app_tests;