beam/arena.rs
1//! Thread-safe bump allocator implementing `allocator-api2`'s `Allocator` trait.
2//!
3//! ## Why not `bumpalo`?
4//!
5//! [`bumpalo::Bump`] is `!Send + !Sync` — bump allocation is inherently
6//! single-threaded because the bump pointer is a plain `Cell<usize>`. BEAM's
7//! tokio multi-threaded runtime sends `Arc<Put>` between worker threads via
8//! channels, which requires `Put: Send`. If the `BTreeMap` inside `Put` uses
9//! a `!Send` allocator, the entire `Put` becomes `!Send` and cannot cross
10//! thread boundaries.
11//!
12//! ## Design
13//!
14//! `SyncBumpArena` wraps a `Mutex<SyncBumpInner>` behind an `Arc`. The mutex
15//! is only contended during **allocation** (construction of BTreeMap nodes).
16//! Read-only access to the BTreeMap (lookups, iteration, serialization) does
17//! not touch the allocator at all — the `Allocator` trait's `allocate` and
18//! `deallocate` methods are only called during insertion and drop.
19//!
20//! The inner state holds a chunk list (`Vec<Chunk>`) and a cursor pointing
21//! into the current chunk. When the current chunk is exhausted, a new chunk
22//! is allocated from the global allocator (doubling in size, starting at
23//! 4 KiB). `deallocate` is a no-op — all memory is freed at once when the
24//! last `Arc` reference drops.
25//!
26//! ## Performance
27//!
28//! - **Allocation**: `Mutex::lock` + pointer bump. The lock is held for
29//! nanoseconds (just advancing a cursor). In practice, contention is rare
30//! because BTreeMap construction happens in a single actor's context.
31//! - **Drop**: O(chunks) — free each chunk. O(1) relative to the number of
32//! entries in the BTreeMap. This is the primary win: std's BTreeMap drop
33//! walks every node; ours just frees a few chunks.
34//! - **Clone**: `Arc::clone` — one atomic increment.
35//!
36//! ## Safety
37//!
38//! The `unsafe impl Allocator` delegates to `SyncBumpInner::allocate`,
39//! which returns valid, aligned memory from a chunk. The memory remains
40//! valid until the `Arc<SyncBumpInner>` is dropped (i.e., until all clones
41//! of the arena are gone). `deallocate` is a no-op, which is sound for bump
42//! allocators.
43
44use allocator_api2::alloc::{AllocError, Allocator, Global, Layout};
45use std::ptr::NonNull;
46use std::sync::{Arc, Mutex};
47
48// ───────────────────────────────────────────────────────────────────────
49// Chunk — a single contiguous block of memory
50// ───────────────────────────────────────────────────────────────────────
51
52/// A contiguous block of memory used by the bump allocator.
53///
54/// Memory is allocated from the global allocator and freed on drop.
55struct Chunk {
56 /// The backing memory, allocated via `Global`.
57 /// Stored as `Vec<u8>` for automatic drop — when the `Chunk` is dropped,
58 /// the `Vec` returns its memory to the global allocator.
59 data: Vec<u8>,
60}
61
62impl Chunk {
63 /// Allocates a new chunk of the given size.
64 fn new(size: usize) -> Self {
65 // Use `Layout` for proper alignment on the Vec's backing allocation.
66 // We align to 16 to cover most BTreeMap node types.
67 let layout = Layout::from_size_align(size, 16).expect("invalid layout");
68 let ptr = Global.allocate(layout).expect("global alloc failed").cast();
69 // SAFETY: we just allocated `size` bytes with alignment 16.
70 let data = unsafe { Vec::from_raw_parts(ptr.as_ptr(), 0, size) };
71 Self { data }
72 }
73
74 /// Returns the usable capacity of this chunk.
75 #[inline]
76 fn capacity(&self) -> usize {
77 self.data.capacity()
78 }
79
80 /// Returns a raw pointer to the start of the chunk's unused region.
81 #[inline]
82 fn start(&self) -> *const u8 {
83 self.data.as_ptr()
84 }
85}
86
87// ───────────────────────────────────────────────────────────────────────
88// SyncBumpInner — the actual bump allocator state (behind Mutex)
89// ───────────────────────────────────────────────────────────────────────
90
91/// Internal state of the bump allocator, protected by a `Mutex`.
92struct SyncBumpInner {
93 /// Chunks of backing memory. The last chunk is the "current" one being
94 /// bumped. Previous chunks are full.
95 chunks: Vec<Chunk>,
96 /// Offset (in bytes) into the current chunk where the next allocation
97 /// will start.
98 cursor: usize,
99 /// The capacity of the current (last) chunk.
100 current_cap: usize,
101}
102
103impl SyncBumpInner {
104 /// Creates a new empty inner state.
105 fn new() -> Self {
106 Self {
107 chunks: Vec::new(),
108 cursor: 0,
109 current_cap: 0,
110 }
111 }
112
113 /// Creates a new inner state with an initial chunk of the given size.
114 fn with_capacity(cap: usize) -> Self {
115 let chunk = Chunk::new(cap);
116 let cap = chunk.capacity();
117 Self {
118 chunks: vec![chunk],
119 cursor: 0,
120 current_cap: cap,
121 }
122 }
123
124 /// Allocates `layout.size()` bytes with `layout.align()` alignment from
125 /// the bump arena. Returns a `NonNull<[u8]>` slice.
126 ///
127 /// If the current chunk doesn't have enough space, a new chunk is
128 /// allocated (doubling in size, minimum 4 KiB).
129 fn allocate(&mut self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
130 let size = layout.size();
131 let align = layout.align();
132
133 // Handle zero-sized allocations — return a dangling but aligned pointer.
134 if size == 0 {
135 return Ok(NonNull::slice_from_raw_parts(NonNull::dangling(), 0));
136 }
137
138 // Try the current chunk first.
139 if let Some(ptr) = self.try_alloc_in_current(align, size) {
140 return Ok(NonNull::slice_from_raw_parts(
141 NonNull::new(ptr).expect("non-null from valid chunk"),
142 size,
143 ));
144 }
145
146 // Current chunk is full — grow.
147 self.grow(align, size);
148
149 // Try again with the new chunk.
150 let ptr = self
151 .try_alloc_in_current(align, size)
152 .expect("freshly grown chunk must have room");
153 Ok(NonNull::slice_from_raw_parts(
154 NonNull::new(ptr).expect("non-null from valid chunk"),
155 size,
156 ))
157 }
158
159 /// Attempts to allocate from the current chunk, aligning `cursor` to
160 /// `align` and checking that `size` bytes fit. Returns `Some(ptr)` on
161 /// success, `None` if the chunk is full.
162 #[inline]
163 fn try_alloc_in_current(&mut self, align: usize, size: usize) -> Option<*mut u8> {
164 let chunk = self.chunks.last()?;
165 let base = chunk.start() as usize;
166 let offset = self.cursor;
167
168 // Align the cursor upward.
169 let aligned_offset = (base + offset + align - 1) & !(align - 1);
170 let padding = aligned_offset - base - offset;
171 let new_cursor = offset + padding + size;
172
173 if new_cursor > self.current_cap {
174 return None;
175 }
176
177 self.cursor = new_cursor;
178 Some(aligned_offset as *mut u8)
179 }
180
181 /// Allocates a new chunk large enough for the requested allocation.
182 /// Chunk size doubles each time, starting at 4 KiB.
183 fn grow(&mut self, _align: usize, size: usize) {
184 // Minimum chunk size is 4 KiB. Double from the last chunk, but ensure
185 // we have at least `size` bytes available.
186 let next_size = (self.current_cap * 2).max(4096).max(size);
187
188 let chunk = Chunk::new(next_size);
189 self.current_cap = chunk.capacity();
190 self.cursor = 0;
191 self.chunks.push(chunk);
192 }
193}
194
195// ───────────────────────────────────────────────────────────────────────
196// SyncBumpArena — the public Allocator impl (Arc<Mutex<SyncBumpInner>>)
197// ───────────────────────────────────────────────────────────────────────
198
199/// A thread-safe bump arena allocator implementing `allocator-api2`'s
200/// `Allocator` trait.
201///
202/// Holds an `Arc<Mutex<SyncBumpInner>>` so it is `Clone + Send + Sync + 'static`.
203/// All clones share the same arena — when the last clone drops, all memory is
204/// freed at once (the chunks' `Vec` drop returns memory to the global allocator).
205///
206/// See the [module-level documentation](self) for the full design rationale
207/// and safety model.
208#[derive(Clone)]
209pub struct SyncBumpArena {
210 inner: Arc<Mutex<SyncBumpInner>>,
211}
212
213impl SyncBumpArena {
214 /// Creates a new `SyncBumpArena` with no initial capacity.
215 ///
216 /// The first allocation will trigger a 4 KiB chunk allocation.
217 #[inline]
218 #[must_use]
219 pub fn new() -> Self {
220 Self {
221 inner: Arc::new(Mutex::new(SyncBumpInner::new())),
222 }
223 }
224
225 /// Creates a new `SyncBumpArena` with an initial chunk of the given size.
226 ///
227 /// This avoids a growth step on the first allocations if the approximate
228 /// total size is known in advance.
229 #[inline]
230 #[must_use]
231 pub fn with_capacity(capacity: usize) -> Self {
232 Self {
233 inner: Arc::new(Mutex::new(SyncBumpInner::with_capacity(capacity))),
234 }
235 }
236}
237
238impl Default for SyncBumpArena {
239 #[inline]
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245impl std::fmt::Debug for SyncBumpArena {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.debug_struct("SyncBumpArena")
248 .field("inner", &Arc::as_ptr(&self.inner))
249 .finish()
250 }
251}
252
253// SAFETY: `allocate` delegates to `SyncBumpInner::allocate` which returns
254// valid, aligned memory from a chunk. The memory remains valid until the
255// `Arc<Mutex<SyncBumpInner>>` is dropped (when all clones of the arena are
256// gone). `deallocate` is a no-op — sound for bump allocators because memory
257// is freed in bulk on drop, never per-allocation.
258unsafe impl Allocator for SyncBumpArena {
259 #[inline]
260 fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
261 let mut inner = self.inner.lock().expect("mutex poisoned");
262 inner.allocate(layout)
263 }
264
265 #[inline]
266 unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
267 // No-op: all memory is freed when the arena drops.
268 }
269
270 #[inline]
271 fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
272 let ptr = self.allocate(layout)?;
273 // SAFETY: the memory is valid and we own it exclusively (just allocated).
274 unsafe { ptr.cast::<u8>().as_ptr().write_bytes(0, layout.size()) };
275 Ok(ptr)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use allocator_api2::alloc::Layout;
283
284 #[test]
285 fn test_allocate_basic() {
286 let arena = SyncBumpArena::new();
287 let layout = Layout::from_size_align(64, 8).unwrap();
288 let ptr = arena.allocate(layout).unwrap();
289 assert_eq!(ptr.len(), 64);
290 // Verify the memory is usable
291 unsafe {
292 ptr.cast::<u8>().as_ptr().write_bytes(0xAB, 64);
293 }
294 }
295
296 #[test]
297 fn test_allocate_alignment() {
298 let arena = SyncBumpArena::new();
299 for &align in &[1usize, 2, 4, 8, 16, 32, 64, 128] {
300 let layout = Layout::from_size_align(1, align).unwrap();
301 let ptr = arena.allocate(layout).unwrap();
302 let addr = ptr.cast::<u8>().as_ptr() as usize;
303 assert_eq!(addr % align, 0, "alignment {} not respected", align);
304 }
305 }
306
307 #[test]
308 fn test_allocate_zeroed() {
309 let arena = SyncBumpArena::new();
310 let layout = Layout::from_size_align(128, 8).unwrap();
311 let ptr = arena.allocate_zeroed(layout).unwrap();
312 let slice = unsafe { ptr.as_ref() };
313 assert!(
314 slice.iter().all(|&b| b == 0),
315 "allocate_zeroed returned non-zero memory"
316 );
317 }
318
319 #[test]
320 fn test_zero_size_allocation() {
321 let arena = SyncBumpArena::new();
322 let layout = Layout::from_size_align(0, 1).unwrap();
323 let ptr = arena.allocate(layout).unwrap();
324 assert_eq!(ptr.len(), 0);
325 }
326
327 #[test]
328 fn test_clone_shares_arena() {
329 let arena1 = SyncBumpArena::new();
330 let arena2 = arena1.clone();
331
332 let layout = Layout::from_size_align(8, 8).unwrap();
333 let ptr1 = arena1.allocate(layout).unwrap();
334 let ptr2 = arena2.allocate(layout).unwrap();
335
336 // Both allocations from the same arena — distinct addresses
337 let addr1 = ptr1.cast::<u8>().as_ptr() as usize;
338 let addr2 = ptr2.cast::<u8>().as_ptr() as usize;
339 assert_ne!(addr1, addr2);
340 }
341
342 #[test]
343 fn test_deallocate_is_noop() {
344 let arena = SyncBumpArena::new();
345 let layout = Layout::from_size_align(32, 8).unwrap();
346 let ptr = arena.allocate(layout).unwrap();
347
348 // deallocate should not crash
349 unsafe { arena.deallocate(ptr.cast(), layout) };
350
351 // Memory is still valid after deallocate
352 unsafe {
353 ptr.cast::<u8>().as_ptr().write_bytes(0xCD, 32);
354 }
355 }
356
357 #[test]
358 fn test_chunk_growth() {
359 // Start with a tiny capacity — force growth
360 let arena = SyncBumpArena::with_capacity(64);
361
362 // Allocate more than the initial capacity
363 let layout = Layout::from_size_align(128, 8).unwrap();
364 let ptr = arena.allocate(layout).unwrap();
365 assert_eq!(ptr.len(), 128);
366
367 // Further allocations should work after growth
368 let layout2 = Layout::from_size_align(256, 8).unwrap();
369 let ptr2 = arena.allocate(layout2).unwrap();
370 assert_eq!(ptr2.len(), 256);
371 }
372
373 #[test]
374 fn test_many_allocations() {
375 let arena = SyncBumpArena::with_capacity(4096);
376 let layout = Layout::from_size_align(48, 8).unwrap();
377
378 // Allocate many times — should trigger multiple chunk growths
379 let mut ptrs = Vec::new();
380 for _ in 0..1000 {
381 ptrs.push(arena.allocate(layout).unwrap());
382 }
383
384 // All pointers should be distinct
385 let addrs: Vec<usize> = ptrs
386 .iter()
387 .map(|p| p.cast::<u8>().as_ptr() as usize)
388 .collect();
389 let unique: std::collections::HashSet<_> = addrs.iter().collect();
390 assert_eq!(
391 unique.len(),
392 1000,
393 "all 1000 allocations should be at distinct addresses"
394 );
395 }
396
397 #[test]
398 fn test_drop_frees_memory() {
399 // Verify Arc refcounting works — dropping all clones frees the arena.
400 let arena1 = SyncBumpArena::new();
401 let arena2 = arena1.clone();
402
403 drop(arena1);
404 // arena2 still alive — allocation should work
405 let layout = Layout::from_size_align(16, 8).unwrap();
406 let _ = arena2.allocate(layout).unwrap();
407
408 drop(arena2);
409 // No way to test memory freeing directly, but no crash = success
410 }
411
412 #[test]
413 fn test_with_capacity() {
414 let arena = SyncBumpArena::with_capacity(8192);
415 let layout = Layout::from_size_align(4096, 8).unwrap();
416 let ptr = arena.allocate(layout).unwrap();
417 assert_eq!(ptr.len(), 4096);
418 }
419
420 #[test]
421 fn test_send_sync_bounds() {
422 // Compile-time verification that SyncBumpArena is Send + Sync
423 fn assert_send_sync<T: Send + Sync>() {}
424 assert_send_sync::<SyncBumpArena>();
425 }
426
427 #[test]
428 fn test_concurrent_allocation() {
429 use std::sync::Arc as StdArc;
430 use std::thread;
431
432 let arena = StdArc::new(SyncBumpArena::with_capacity(4096 * 4));
433 let layout = Layout::from_size_align(64, 8).unwrap();
434
435 // Spawn multiple threads that allocate from the same arena
436 let handles: Vec<_> = (0..4)
437 .map(|_| {
438 let arena = StdArc::clone(&arena);
439 thread::spawn(move || {
440 let mut ptrs = Vec::new();
441 for _ in 0..100 {
442 ptrs.push(arena.allocate(layout).unwrap());
443 }
444 // Verify all allocations are distinct
445 let addrs: Vec<usize> = ptrs
446 .iter()
447 .map(|p| p.cast::<u8>().as_ptr() as usize)
448 .collect();
449 let unique: std::collections::HashSet<_> = addrs.iter().collect();
450 assert_eq!(unique.len(), 100);
451 })
452 })
453 .collect();
454
455 for handle in handles {
456 handle.join().expect("thread should not panic");
457 }
458 }
459
460 #[test]
461 fn test_btreemap_with_arena() {
462 use arena_btreemap::BTreeMap;
463
464 let arena = SyncBumpArena::with_capacity(4096);
465 let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena);
466
467 // Insert entries — each BTreeMap node is allocated from the arena
468 for i in 0..100 {
469 map.insert(format!("key_{:04}", i), i);
470 }
471
472 // Verify entries are correct
473 for i in 0..100 {
474 assert_eq!(map.get(&format!("key_{:04}", i)), Some(&i));
475 }
476
477 // Verify iteration order (sorted by key)
478 let keys: Vec<_> = map.keys().take(5).collect();
479 assert_eq!(keys[0], "key_0000");
480 assert_eq!(keys[4], "key_0004");
481 }
482
483 #[test]
484 fn test_btreemap_clone_shares_arena() {
485 use arena_btreemap::BTreeMap;
486
487 let arena = SyncBumpArena::with_capacity(4096);
488 let mut map: BTreeMap<String, i32, SyncBumpArena> = BTreeMap::new_in(arena.clone());
489 map.insert("a".to_string(), 1);
490 map.insert("b".to_string(), 2);
491
492 // Clone the map — should share the same arena
493 let map2 = map.clone();
494 assert_eq!(map2.get("a"), Some(&1));
495 assert_eq!(map2.get("b"), Some(&2));
496
497 // Original map still works
498 assert_eq!(map.get("a"), Some(&1));
499 }
500}