Arena Allocation Concepts
Arena allocation (also known as region-based or slab allocation) groups the allocation of many nodes into a single backing container—either a contiguous Vec indexed by handles or a bump-allocated memory chunk—and ties their lifetimes together. In Rust, arenas solve two problems simultaneously: they eliminate thousands of individual heap allocations and allow cyclic or bidirectional graph/tree links (parent, prev, next) without Rc<RefCell<T>> or unsafe code!
Individual `Box` / `Rc` Allocations (Scattered Across Heap):
[ Node 0 ] ---> (RAM miss) ---> [ Node 1 ] ---> (RAM miss) ---> [ Node 2 ]
Contiguous Index / Generational Arena (`Vec<Slot<T>>`):
+-----------------------------------------------------------------------+
| Slot 0 (gen=1) | Slot 1 (gen=1) | Slot 2 (gen=3, Free) | Slot 3 (gen=1)|
+-----------------------------------------------------------------------+
Referenced by lightweight 8-byte `ArenaKey { index: u32, generation: u32 }`!
Three Types of Arenas in Rust
| Arena Pattern | How References Are Represented | Supports Individual Deletion? | Ecosystem Crates |
|---|---|---|---|
1. Simple Vec<T> Index Arena |
struct Id(u32) index into Vec<T> |
Append-only (or tombstone Option<T>) |
Built-in Vec<T>, la-arena |
2. Generational Arena (SlotMap) |
Key { index: u32, generation: u32 } |
Yes ($O(1)$ removal + prevents ABA stale-key bugs!) | slotmap, thunderdome, slab |
3. Lifetime Bump Arena (&'arena T) |
Real Rust references &'arena T / &'arena mut T |
Freed all at once when the Arena is dropped |
bumpalo, typed-arena |
Solving the Stale-Index ("ABA") Problem with a Generational Arena
In a simple Vec<Option<T>> slab where freed indices are reused by a free list, if you delete the item at index 2 and insert a new item that reuses index 2, an old stale Id(2) handle sitting elsewhere in the program will accidentally access the wrong new object (the ABA problem)!
A generational arena attaches a monotonically increasing generation: u32 counter to every slot in the vector and to every returned HandleId. When a slot is freed, its generation counter increments—so any stale HandleId with the old generation is rejected in $O(1)$ time with None!
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArenaKey {
index: u32,
generation: u32,
}
enum Slot<T> {
Occupied { generation: u32, value: T },
Vacant { generation: u32, next_free: Option<u32> },
}
pub struct GenerationalArena<T> {
slots: Vec<Slot<T>>,
free_head: Option<u32>,
len: usize,
}
impl<T> GenerationalArena<T> {
pub const fn new() -> Self {
Self {
slots: Vec::new(),
free_head: None,
len: 0,
}
}
pub fn insert(&mut self, value: T) -> ArenaKey {
self.len += 1;
if let Some(idx) = self.free_head {
let slot = &mut self.slots[idx as usize];
match *slot {
Slot::Vacant { generation, next_free } => {
self.free_head = next_free;
*slot = Slot::Occupied { generation, value };
ArenaKey { index: idx, generation }
}
Slot::Occupied { .. } => unreachable!("free list pointed to occupied slot"),
}
} else {
let index = self.slots.len() as u32;
let generation = 1;
self.slots.push(Slot::Occupied { generation, value });
ArenaKey { index, generation }
}
}
pub fn get(&self, key: ArenaKey) -> Option<&T> {
match self.slots.get(key.index as usize)? {
Slot::Occupied { generation, value } if *generation == key.generation => Some(value),
_ => None, // Slot is either vacant OR was reused by a newer generation!
}
}
pub fn remove(&mut self, key: ArenaKey) -> Option<T> {
let slot = self.slots.get_mut(key.index as usize)?;
match slot {
Slot::Occupied { generation, .. } if *generation == key.generation => {
let next_gen = generation.wrapping_add(1);
let old = std::mem::replace(
slot,
Slot::Vacant {
generation: next_gen,
next_free: self.free_head,
},
);
self.free_head = Some(key.index);
self.len -= 1;
match old {
Slot::Occupied { value, .. } => Some(value),
_ => unreachable!(),
}
}
_ => None,
}
}
}
fn main() {
let mut arena = GenerationalArena::new();
let key_old = arena.insert("Player_1");
assert_eq!(arena.get(key_old), Some(&"Player_1"));
// Remove "Player_1" (slot 0's generation increments from 1 -> 2):
assert_eq!(arena.remove(key_old), Some("Player_1"));
// Insert "Player_2", which reuses slot 0 at generation 2:
let key_new = arena.insert("Player_2");
assert_eq!(key_old.index, key_new.index);
assert_ne!(key_old.generation, key_new.generation);
// Stale `key_old` safely returns `None` instead of reading "Player_2"!
assert_eq!(arena.get(key_old), None);
assert_eq!(arena.get(key_new), Some(&"Player_2"));
println!(
"Stale key rejected ({:?}), new key resolved: {:?}",
arena.get(key_old),
arena.get(key_new)
);
}
Stale key rejected (None), new key resolved: Some("Player_2")