dodgems/lib.rs
1#![no_std]
2#![feature(allocator_api)]
3#![feature(doc_auto_cfg)]
4//! # Dodgems - A simple bump allocator library
5//!
6//! This crate provides a fast, single-threaded [bump allocator](BumpCar) for use in performance
7//! sensitive contexts.
8//!
9//! **⚠️ It is not a general purpose allocator: you need to have another
10//! (supposedly slower) allocator to back it up.** By default, it is the global allocator.
11//!
12//! It can be used for quick and dirty allocation in a loop, where you know memory
13//! can be reclaimed all at once at the end.
14//!
15//! ## Example
16//! ```rust
17//! #![feature(allocator_api)]
18//! # #[cfg(feature = "alloc")]
19//! # {
20//! use dodgems::BumpCar;
21//!
22//! let mut bumpcar = BumpCar::new(1024).unwrap(); // 1kB capacity
23//!
24//! for i in 0..100 {
25//! let mut v = Vec::new_in(&bumpcar); // allocate with the allocator api
26//! v.push(42);
27//! // small fast allocations in hot loop
28//! drop(v);
29//! bumpcar.reset(); // reset the capacity once every allocation has been dropped
30//! }
31//!
32//! drop(bumpcar)
33//! # }
34//! ```
35//!
36//! Until the `allocator_api` is stable, this crate requires the nightly edition.
37//!
38//! ## Features
39//! The (default) `alloc` feature controls wether the `alloc` standard crate is used.
40//! If you want to use a different allocator and/or do not have a global allocator available,
41//! you can disable it.
42
43#[cfg(feature = "alloc")]
44extern crate alloc;
45
46#[cfg(feature = "alloc")]
47use alloc::alloc::Global;
48use core::alloc::{AllocError, Allocator, Layout};
49use core::{cell::Cell, mem::size_of, ptr::NonNull};
50
51/// Returns the next multiple of `align` greater than `size`
52///
53/// # Safety
54/// `size + align` must not overflow, and `align` must be a power of two.
55///
56/// As a consequence, [`dodgems`](crate) does not support alignment values above 2^30.
57unsafe fn next_multiple(size: usize, align: usize) -> usize {
58 let am = align - 1;
59 (size + am) & !am
60}
61
62#[cfg(test)]
63#[test]
64fn next_multiple_power_of_two() {
65 for size in 0..512 {
66 for align in [1, 2, 4, 8, 16, 32, 64] {
67 let nm = unsafe { next_multiple(size, align) };
68 assert_eq!(nm % align, 0);
69 let q = nm / align;
70 assert!(q * align >= size);
71 if q > 0 {
72 assert!((q - 1) * align < size);
73 }
74 }
75 }
76}
77
78/// Fast bump allocator.
79///
80/// Allocations are made by incrementing an offset, and are tied to the lifetime of a reference
81/// to the allocation until the [`BumpCar`] is dropped or reset.
82///
83/// # Example
84/// ```rust
85/// #![feature(allocator_api)]
86/// # extern crate alloc;
87/// # use alloc::alloc::Global;
88/// use dodgems::BumpCar;
89///
90/// let mut bumpcar = BumpCar::new_in(256, Global).unwrap();
91/// let my_box = Box::new_in([1, 2, 3], &bumpcar);
92///
93/// // drop(bumpcar) <- doesn't compile
94/// drop(my_box);
95/// drop(bumpcar);
96/// ```
97pub struct BumpCar<
98 #[cfg(feature = "alloc")] A: Allocator = Global,
99 #[cfg(not(feature = "alloc"))] A: Allocator,
100> {
101 pointer: NonNull<[u8]>,
102 position: Cell<usize>,
103 allocator: A,
104}
105
106impl<A: Allocator> BumpCar<A> {
107 /// Allocates a new [`BumpCar`] in the given allocator.
108 ///
109 /// # Errors
110 /// This function returns an error if the capacity (or the nearest pointer-aligned multiple)
111 /// is greater than [`isize::MAX`], or if the underlying allocator returns an error.
112 #[allow(clippy::missing_panics_doc)]
113 pub fn new_in(capacity: usize, allocator: A) -> Result<Self, AllocError> {
114 // SAFETY: capacity must be <= isize::MAX for next_multiple to be evaluated,
115 // and size_of::<usize>() is a power of two.
116 if capacity > isize::MAX as _
117 || unsafe { next_multiple(capacity, size_of::<usize>()) } > isize::MAX as _
118 {
119 return Err(AllocError);
120 }
121
122 let pointer = allocator
123 .allocate(Layout::from_size_align(capacity, core::mem::size_of::<usize>()).unwrap())?;
124
125 Ok(Self {
126 pointer,
127 position: Cell::new(0),
128 allocator,
129 })
130 }
131
132 /// Returns the capacity of the [`BumpCar`].
133 pub fn capacity(&self) -> usize {
134 self.pointer.len()
135 }
136
137 /// Returns the remaining capacity of the [`BumpCar`].
138 ///
139 /// This does not guarantee that an allocation of this size will succeed:
140 /// the [`BumpCar`] will waste space if a new allocation with a greater alignment
141 /// than the last one is required.
142 ///
143 /// If you need to check for the validity of an allocation in a more precise way,
144 /// use [`BumpCar::can_allocate`].
145 pub fn remaining_capacity(&self) -> usize {
146 self.capacity() - self.position.get()
147 }
148
149 /// Checks wether the allocator has enough remaining capacity for the
150 /// allocation specified in `layout`.
151 pub fn can_allocate(&self, layout: Layout) -> bool {
152 // SAFETY: layout.align() is guaranteed to be a power of two,
153 // and self.position() <= pointer.len() <= isize::MAX, so the operation cannot overflow.
154 let closest_align = unsafe { next_multiple(self.position.get(), layout.align()) };
155
156 let Some(new_pos) = closest_align.checked_add(layout.size()) else {
157 return false;
158 };
159
160 new_pos <= self.pointer.len()
161 }
162
163 /// Resets the [`BumpCar`]'s remaining capacity to its initial capacity.
164 ///
165 /// This requires a mutable reference, so that any previous allocations made with &self
166 /// are invalidated by the borrow checker.
167 pub fn reset(&mut self) {
168 self.position.set(0);
169 }
170
171 /// Create a new checkpoint.
172 ///
173 /// This function allocates the rest of the allocated space into another [`BumpCar`],
174 /// that can used to reset part of the allocated space instead of the whole allocation.
175 ///
176 /// # Example
177 /// ```rust
178 /// #![feature(allocator_api)]
179 /// # extern crate alloc;
180 /// use dodgems::BumpCar;
181 ///
182 /// let bumpcar = BumpCar::new(256).unwrap();
183 /// let alloc_half = Box::new_in([0u8; 128], &bumpcar);
184 ///
185 /// let mut checkpoint = bumpcar.checkpoint();
186 /// let alloc_rest = Box::new_in([0u8; 128], &checkpoint);
187 /// drop(alloc_rest);
188 /// assert_eq!(checkpoint.remaining_capacity(), 0);
189 ///
190 /// checkpoint.reset();
191 /// assert_eq!(checkpoint.remaining_capacity(), 128);
192 /// ```
193 pub fn checkpoint<'a>(&'a self) -> BumpCar<&'a BumpCar<A>> {
194 BumpCar::new_in(
195 self.remaining_capacity() - self.remaining_capacity() % size_of::<usize>(),
196 self,
197 )
198 .unwrap()
199 }
200}
201
202#[cfg(feature = "alloc")]
203impl BumpCar {
204 /// Allocates a [`BumpCar`] with the Global allocator.
205 ///
206 /// # Errors
207 /// This function returns an error if the capacity (or its nearest pointer-aligned multiple)
208 /// is greater than [`isize::MAX`], or if the global returns an error.
209 pub fn new(capacity: usize) -> Result<Self, AllocError> {
210 Self::new_in(capacity, Global)
211 }
212}
213
214impl<A: Allocator> Drop for BumpCar<A> {
215 /// Deallocates the [`BumpCar`]'s buffer.
216 fn drop(&mut self) {
217 let ptr = self.pointer.cast::<u8>();
218 // SAFETY: ptr is always allocated with self.allocator
219 // and the alignement has been validated at construction of the BumpCar
220 unsafe {
221 self.allocator.deallocate(
222 ptr,
223 Layout::from_size_align_unchecked(self.pointer.len(), size_of::<usize>()),
224 );
225 }
226 }
227}
228
229unsafe impl<A: Allocator> Allocator for &BumpCar<A> {
230 fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
231 // SAFETY: layout.align() is guaranteed to be a power of two,
232 // and self.position() <= pointer.len() <= isize::MAX, so the operation cannot overflow.
233 let closest_align = unsafe { next_multiple(self.position.get(), layout.align()) };
234
235 let new_pos = closest_align.checked_add(layout.size()).ok_or(AllocError)?;
236 if new_pos > self.pointer.len() {
237 return Err(AllocError);
238 }
239
240 // SAFETY: closest_align + layout.size() <= pointer.len() <= isize::MAX
241 let ptr = unsafe { self.pointer.as_ptr().cast::<u8>().add(closest_align) };
242 self.position.set(new_pos);
243 Ok(NonNull::slice_from_raw_parts(
244 // SAFETY: pointer is non null, and closest_align + layout.size() <= pointer.len(),
245 // so ptr = pointer + closest_align is non null.
246 unsafe { NonNull::new_unchecked(ptr) },
247 layout.size(),
248 ))
249 }
250
251 /// The [`BumpCar`] does not perform deallocation unless it's reset or dropped.
252 unsafe fn deallocate(&self, _: NonNull<u8>, _: Layout) {}
253
254 /// Shrinks an allocated region.
255 ///
256 /// The [`BumpCar`] allocator has the extra requirement
257 /// that the old layout's alignment MUST be bigger than the new one.
258 unsafe fn shrink(
259 &self,
260 ptr: NonNull<u8>,
261 old_layout: Layout,
262 new_layout: Layout,
263 ) -> Result<NonNull<[u8]>, AllocError> {
264 debug_assert!(
265 new_layout.size() <= old_layout.size(),
266 "`new_layout.size()` must be smaller than or equal to `old_layout.size()`"
267 );
268 if old_layout.align() < new_layout.align() {
269 return Err(AllocError);
270 }
271
272 Ok(NonNull::slice_from_raw_parts(ptr, new_layout.size()))
273 }
274}