fastarena
A zero-dependency bump-pointer arena allocator with RAII transactions, nested savepoints, optional destructor tracking, and ArenaVec — built for compilers, storage engines, and high-throughput request-scoped workloads.
Why FastArena?
| Feature | Description |
|---|---|
| Zero-copy | Allocations return direct references, no indirection |
| O(1) allocation | Single bounds check + bump pointer advance |
| Zero-cost reset | Reuse all memory without OS calls or page faults |
| Transactions | RAII guard with commit/rollback, nested savepoints |
| Drop-tracking | Opt-in destructor execution — zero-cost when off |
| Budget enforcement | Cap bytes per transaction for request-scoped safety |
| ArenaBox | Owned allocation type (Box-like) for ownership without heap |
Quick Start
Basic Allocation
use Arena;
let mut arena = new;
let x: &mut u64 = arena.alloc;
let squares: &mut = arena.alloc_slice;
let s: &str = arena.alloc_str;
// Zero-cost reset — pages stay warm, no OS calls
arena.reset;
Transactions — Auto-Rollback on Failure
No other arena allocator gives you RAII transactions. Allocations succeed or roll back as a unit — no leaks, no manual cleanup.
Note: Each
alloccall returns a&mut Tthat borrows the transaction. Don't hold references across subsequent allocations — use the value and let it drop before callingallocagain.
use Arena;
let mut arena = new;
// Ok commits, Err rolls back — all allocations are atomic
let result: = arena.with_transaction;
assert_eq!;
// Failed transaction — everything allocated inside is gone
arena.with_transaction;
// Infallible variant — commits even through panic
let val = arena.with_transaction_infallible;
assert_eq!;
Nested Savepoints
Transactions nest to arbitrary depth. Each savepoint is independently committable — roll back an inner scope without losing outer work.
use Arena;
let mut arena = new;
let mut outer = arena.transaction;
outer.alloc_str;
outer.alloc_str;
outer.commit; // "top-level" + "confirmed" survive
Multiple Allocations and the Borrow Checker
All alloc* methods return &mut T. This prevents making multiple allocations simultaneously because the borrow checker sees the arena as mutably borrowed. Workarounds include immediate consumption, raw pointers, ArenaVec, and ArenaBox<T>:
use ;
let mut arena = new;
let x: = arena.alloc_box;
// ArenaBox derefs to &T / &mut T like Box<T>
assert_eq!;
// Drop runs T's destructor in place; arena memory is reclaimed in bulk
// later by reset() / rewind() / arena drop. No drop-tracking feature
// required — semantics match std::boxed::Box<T>.
ArenaVec with finish() — Transfer Ownership to the Arena
ArenaVec is a growable vector backed by arena memory. Call finish() to hand ownership to the arena — the slice lives as long as the arena, no copy. With drop-tracking enabled, the elements are registered for destruction on the next reset / rewind; without the feature, no destructors run.
use Arena;
use ArenaVec;
let mut arena = new;
let items: &mut = ;
assert_eq!;
assert_eq!;
ArenaVec also implements Deref<Target = [T]>, so all slice methods work directly:
use ;
let mut arena = new;
let mut v = new;
v.extend_exact;
v.sort; // direct slice method
v.dedup; // ArenaVec helper
assert_eq!;
Transaction Budgets — Cap Memory per Request
Set a byte budget on any transaction. Exceed it and alloc panics (or try_alloc returns None). Zero-cost when unlimited.
Note: The budget tracks bytes written to arena blocks only. Heap allocations inside values (e.g.
Vec,String) are not tracked — usealloc_sliceoralloc_slice_copyto budget actual data bytes.
use Arena;
let mut arena = new;
let mut txn = arena.transaction;
txn.set_limit; // hard cap
// GOOD — alloc_slice copies data into arena, budget sees all bytes
txn.alloc_slice; // ok — 2048 arena bytes
// txn.alloc_slice(vec![0u8; 4096]); // panics: budget exceeded (2048 + 4096 > 4096)
// BAD — alloc(vec![...]) stores only the 24-byte Vec struct, heap is untracked
// txn.alloc(vec![0u8; 9999]); // would NOT panic — budget sees 24 bytes
let remaining = txn.budget_remaining; // introspect at any time
txn.commit;
Drop-Tracking — Opt-In Destructor Execution
By default, fastarena never runs destructors (zero overhead). Enable drop-tracking to run them in LIFO order on reset() / rewind().
[]
= { = "0.2", = ["drop-tracking"] }
use Arena;
let mut arena = new;
let cp = arena.checkpoint;
arena.alloc;
arena.alloc;
// With drop-tracking: drops fire in LIFO order ("world", then "hello")
// Without drop-tracking: no destructors, memory reclaimed instantly
arena.rewind;
Use Cases
- Compiler AST / parsers — allocate all nodes per pass, reset in bulk
- Graphs and cyclic structures — same-lifetime references enable safe cycles without
Rc/RefCell - Trees with parent pointers — back-references trivially supported
- Heterogeneous types — allocate
Node,Edge,Tokenin a single arena - Phase-oriented bulk alloc/free — many objects created, bulk-freed via
reset()orrewind() - Request-scoped memory — thread-local arena per HTTP request, zero-cost recycle
- Transactional batch processing — commit on success, auto-rollback on failure, nested savepoints
- Dynamic collections —
ArenaVecwith O(1) push, arena-backed lifetime
See USAGE.md for full examples.
Performance
Medians below mix a fresh Criterion --quick sweep (Linux x86-64) with earlier full runs; re-measure before publishing release notes. Absolute timings vary by CPU, frequency, and Rust version.
Reproducing benchmarks
# Full comparison + in-crate benches (slow)
# Shorter iteration (Criterion)
# Examples: filter by substring
Head-to-head: fastarena vs bumpalo vs typed-arena
| Benchmark | fastarena | bumpalo | typed-arena |
|---|---|---|---|
| alloc 1k items | 995 ns | 1162 ns | 1779 ns |
| alloc_slice n=64 ‡ | 58 ns | 68 ns | 93 ns |
| alloc_slice n=1024 ‡ | 124 ns | 562 ns | — |
| alloc_str (100×) § | 163 ns | 151 ns | — |
| ArenaVec n=16 | 51 ns | 60 ns | 27 ns |
| ArenaVec n=256 | 510 ns | 418 ns | 393 ns |
| ArenaVec n=4096 | 2.9 µs | 9.3 µs | 14.1 µs |
| 10k allocs + reset | 14.4 µs | 14.8 µs | 3.1 µs† |
| reset (1 block) ※ | 25 ns | 35 ns | — |
| reset (4 blocks) ※ | 175 ns | 72 ns | — |
| reset (8 blocks) ※ | 376 ns | 214 ns | — |
| 128 KB alloc | 116 ns | 53 ns | — |
† typed-arena benchmark allocates once per fresh arena in a tight loop; not directly comparable to reset + reuse.
‡ alloc_slice / alloc_slice_copy in arena_bench: one arena per benchmark, reset() each iteration (measures fill + bump, not Block::new per sample).
§ alloc_str ×100: one arena / bump per benchmark, 100 copies then reset() each iteration (request-style reuse).
※ arena_bench reset/*: Arena::with_capacity(64) / matching bump capacity, two bursts of blocks × 8 × u64 allocs separated by reset() (workload-dependent; bumpalo can be faster once multiple chunks are involved).
Fast path benchmarks (vs std Box/Vec)
| Benchmark | fastarena | Box/Vec | Speedup |
|---|---|---|---|
| alloc 1k u64 | 1050 ns | 52.8 µs | ~50× |
| alloc_slice n=512 ‡ | 62 ns | 74 ns | ~1.2× |
| alloc_slice n=4096 ‡ | 231 ns | 463 ns | ~2× |
| 10k allocs + reset | 24.9 µs | 336 µs | ~13× |
Arena::new |
55.7 ns | — | — |
checkpoint() |
204 ns | — | — |
reset 1 block ※ |
25 ns | 35 ns | — |
commit 16 allocs |
1.69 µs | — | — |
Why fastarena excels
- ~4–5× faster
alloc_slicethan bumpalo at n=1024 in these benchmarks (batch fill into arena memory) ArenaVec— faster than bumpalo for larger n (e.g. n=4096); typed-arena can win when n is small (and in this run, up through n=256) where its pattern dominates- Scalar
alloc— competitive with bumpalo (~995 ns vs ~1162 ns for 1k× u64 here) alloc_str(hot loop) — within ~10% of bumpalo when reusing one arena andreset()per batch (§)- ~50× faster than
Boxfor 1k allocs + arenaresetvs 1kBox::new+ drop (seearena_bench) reset— on thearena_benchmicro-workload, single-chunk reuse favors fastarena; multi-chunkresetcan favor bumpalo (※) — always profile your own allocation pattern- Zero dependencies: No external crates required
Feature Flags
[]
= { = "0.2", = ["drop-tracking"] }
| Flag | Default | Description |
|---|---|---|
drop-tracking |
Off | Run destructors in LIFO order on reset/rewind |
When NOT to Use an Arena
- Objects with independent lifetimes — use
Box<T>orRc<T> - Frequent arbitrary-order removal — use a slab allocator
- Thread-shared allocation — wrap in a
Mutexor use thread-local arenas
Documentation
See USAGE.md for complete API reference.