malloc answers a hard question: any size, any time, any thread, freed in
any order, forever. Answering it costs size classes, free lists, arenas-with-
locks, and metadata on every chunk. But look at your actual allocations and a
huge fraction don’t need the hard answer — a compiler pass, an HTTP request,
a game frame, a parse tree: hundreds of allocations, one shared lifetime,
all dead together at a known moment. The allocator matched to that shape is
almost embarrassingly small, and it beats malloc by an order of magnitude
where it applies.
The Whole Allocator
typedef struct { char *base; size_t used, cap; } Arena;
void *arena_alloc(Arena *a, size_t n, size_t align) {
size_t off = (a->used + align - 1) & ~(align - 1);
if (off + n > a->cap) return arena_grow(a, off + n); /* rare path */
a->used = off + n;
return a->base + off;
}
void arena_reset(Arena *a) { a->used = 0; } /* "free" everything */Allocation is an align, an add, a compare — a handful of instructions, no locks, no metadata per object, no size classes. Deallocation is setting one integer. And there is no free(ptr) — that absence is the design, not a gap: lifetime became a property of the arena, decided once at the architecture level, instead of a property tracked per-object at every call site.
What falls out:
- Locality. Consecutive allocations are physically adjacent — the parse tree you build node-by-node is laid out like an array, and walking it prefetches itself. malloc’d nodes of mixed ages scatter.
- The bug class shrinks. No per-object free means no double-free, no “who owns this” ambiguity within the arena, no leak-by-forgotten-free — the leak unit becomes the arena, which is one thing to audit instead of ten thousand.
- Failure handling centralizes. One out-of-memory path at arena growth, not a check after every node allocation.
Where Arenas Are Load-Bearing
This isn’t a niche trick; it’s quietly the standard architecture in several
domains. Compilers arena-allocate ASTs and IR per compilation unit (GCC’s
obstacks since the 80s; rustc and LLVM arena everywhere). Servers use
per-request arenas — Apache’s apr_pool_t made it a public API shape;
nginx’s ngx_pool_t likewise; the pattern kills the per-request leak
entirely, since request teardown resets the pool no matter what the handler
forgot. Games run per-frame arenas: allocate freely during simulation,
reset at frame end, sixty times a second, zero fragmentation forever.
glibc’s own alloca/VLA is a degenerate arena (the stack frame), and
protobuf’s C++ arena mode exists because Google measured parse-tree
allocation dominating RPC costs.
The variations you’ll actually want: scratch/temp arenas passed into
functions for intermediate work (caller resets); checkpointing
(mark = a->used; ...; a->used = mark) for nested lifetimes — a
stack-of-lifetimes inside one arena; and chained blocks for growth, so
addresses stay stable (realloc’ing the arena would invalidate every pointer
into it — the one rookie implementation mistake with real consequences).
The Honest Boundaries
Arenas are a lifetime bet, and mismatched bets cost memory: one long-lived
object forces the whole arena to live (the fix is promoting it — copy out to
a longer arena — which reintroduces ownership thinking at the boundary).
Unbounded or user-controlled lifetimes (caches, sessions, anything with
eviction) belong to general-purpose allocation or refcounting. Freeing
mid-lifetime is possible (arena_reset to a checkpoint) only in strict
LIFO order. And within a thread they’re perfect, but sharing an arena across
threads reintroduces the synchronization malloc already solved — per-thread
arenas is the idiom.
Security note that deserves more airtime: use-after-free within an arena’s
lifetime is impossible by construction, but use-after-reset is the same
bug wearing a new name, and it’s nastier — the memory is valid, warm, and
full of unrelated new objects. ASan’s manual poisoning API
(__asan_poison_memory_region on reset) restores the guardrails; good arena
libraries do it for you.
Takeaways
- Arena = bump pointer + reset; allocation in ~5 instructions, deallocation in one store, zero per-object metadata.
- The design move is grouping by lifetime: per-request, per-frame, per-compilation — teardown becomes O(1) and leak-proof.
- Compilers, web servers, and game engines standardized this decades ago; reach for it whenever many allocations die together.
- Chain blocks (stable addresses), checkpoint for nested scopes, promote long-lived escapees, keep arenas per-thread.
- Use-after-reset is the arena’s UAF — poison on reset under ASan and the bug class stays dead.