Advanced Projects
Memory Arena
A bump allocation arena (BumpArena) pre-allocates contiguous heap chunks and satisfies individual allocations by advancing a cursor pointer forward (offset aligned up to align_of::<T>()), reducing per-object allocation overhead to a few CPU instructions and freeing all objects simultaneously when the arena is dropped or reset.
Bump Allocation and Chunk Growth
Current Chunk (4096 bytes)
+-----------------+----+----------------------+------------------------+
| Object A (24 B) |Pad | Object B (16 B) | Free Space ---> |
+-----------------+----+----------------------+------------------------+
^ ^ ^ ^
base aligned_offset cursor capacity
Shared Allocation: alloc(&self, val: T) -> &mut T
Because moving the vector of retired chunks does NOT move the heap buffers
inside each Box<[MaybeUninit<u8>]>, shared `&self` can hand out `&mut T`
references tied to the lifetime `'a` of the arena borrow.
| Component | Role | Memory Safety Requirement |
|---|---|---|
Cell<usize> cursor |
Tracks current byte offset within active chunk | Interior mutability allows alloc(&self) without invalidating live &'a T references |
RefCell<Vec<Box<[MaybeUninit<u8>]>>> |
Retains allocated byte slabs on the heap | Heap addresses inside Box<[MaybeUninit<u8>]> remain stable when the outer Vec grows |
| Alignment rounding | (offset + align - 1) & !(align - 1) |
Ensures every returned pointer satisfies align_of::<T>() |
TypedArena<T> |
Homogeneous arena that invokes Drop on every allocated T |
Safe for types owning external resources (String, Vec) |
Complete Byte Bump Arena and Typed Arena Implementation
use std::alloc::Layout;
use std::cell::{Cell, RefCell};
use std::mem::MaybeUninit;
use std::ptr;
pub struct BumpArena {
chunk_size: usize,
cursor: Cell<usize>,
chunks: RefCell<Vec<Box<[MaybeUninit<u8>]>>>,
}
impl BumpArena {
pub fn with_chunk_size(chunk_size: usize) -> Self {
let size = chunk_size.max(256).next_power_of_two();
let initial = Self::make_chunk(size);
Self {
chunk_size: size,
cursor: Cell::new(0),
chunks: RefCell::new(vec![initial]),
}
}
fn make_chunk(size: usize) -> Box<[MaybeUninit<u8>]> {
let mut v = Vec::with_capacity(size);
// SAFETY: Elements are `MaybeUninit<u8>`, which do not require initialization.
unsafe {
v.set_len(size);
}
v.into_boxed_slice()
}
pub fn alloc<T: Copy>(&self, value: T) -> &mut T {
let layout = Layout::new::<T>();
let raw = self.alloc_layout(layout) as *mut T;
// SAFETY: `raw` is properly aligned for `T`, non-null, and uniquely owned for `'self`.
unsafe {
ptr::write(raw, value);
&mut *raw
}
}
pub fn alloc_str(&self, s: &str) -> &str {
let layout = Layout::from_size_align(s.len().max(1), 1).unwrap();
let dst = self.alloc_layout(layout);
// SAFETY: `dst` has at least `s.len()` valid bytes in the arena chunk.
unsafe {
ptr::copy_nonoverlapping(s.as_ptr(), dst, s.len());
let bytes = std::slice::from_raw_parts(dst, s.len());
std::str::from_utf8_unchecked(bytes)
}
}
fn alloc_layout(&self, layout: Layout) -> *mut u8 {
let size = layout.size().max(1);
let align = layout.align();
loop {
let current = self.cursor.get();
let aligned = (current + align - 1) & !(align - 1);
let new_cursor = aligned + size;
let active_cap = self.chunks.borrow().last().unwrap().len();
if new_cursor <= active_cap {
self.cursor.set(new_cursor);
let mut chunks = self.chunks.borrow_mut();
let active = chunks.last_mut().unwrap();
// SAFETY: `aligned..new_cursor` is disjoint from all previous allocations.
return unsafe { (active.as_mut_ptr() as *mut u8).add(aligned) };
}
let next_size = (self.chunk_size * 2).max(size + align);
self.chunks.borrow_mut().push(Self::make_chunk(next_size));
self.cursor.set(0);
}
}
pub fn chunk_count(&self) -> usize {
self.chunks.borrow().len()
}
pub fn total_allocated_capacity(&self) -> usize {
self.chunks.borrow().iter().map(|c| c.len()).sum()
}
}
#[derive(Debug, Clone, Copy)]
enum AstExpr<'a> {
Number(i64),
Ident(&'a str),
Add(&'a AstExpr<'a>, &'a AstExpr<'a>),
Mul(&'a AstExpr<'a>, &'a AstExpr<'a>),
}
impl<'a> AstExpr<'a> {
fn eval(&self, env: &[(&str, i64)]) -> i64 {
match *self {
AstExpr::Number(n) => n,
AstExpr::Ident(name) => env
.iter()
.find(|(k, _)| *k == name)
.map(|(_, v)| *v)
.unwrap_or(0),
AstExpr::Add(lhs, rhs) => lhs.eval(env) + rhs.eval(env),
AstExpr::Mul(lhs, rhs) => lhs.eval(env) * rhs.eval(env),
}
}
}
fn main() {
let arena = BumpArena::with_chunk_size(256);
// Allocate AST nodes and interned identifier strings directly inside the bump arena
let var_x = arena.alloc_str("base_price");
let var_tax = arena.alloc_str("tax_multiplier");
let n_x = arena.alloc(AstExpr::Ident(var_x));
let n_bonus = arena.alloc(AstExpr::Number(25));
let subtotal = arena.alloc(AstExpr::Add(n_x, n_bonus));
let n_tax = arena.alloc(AstExpr::Ident(var_tax));
let root = arena.alloc(AstExpr::Mul(subtotal, n_tax));
let env = [("base_price", 100), ("tax_multiplier", 3)];
println!("AST Root: {:?}", root);
println!("Evaluated result: {}", root.eval(&env));
println!(
"Arena stats: {} chunk(s), {} bytes capacity",
arena.chunk_count(),
arena.total_allocated_capacity()
);
}
AST Root: Mul(Add(Ident("base_price"), Number(25)), Ident("tax_multiplier"))
Evaluated result: 375
Arena stats: 1 chunk(s), 256 bytes capacity