# Arena Allocators: Free Everything at Once

> The fastest malloc is a pointer bump; the fastest free is resetting one offset. Arenas trade individual deallocation for speed, locality, and the death of use-after-free within a lifetime — a trade half your workloads should take.

URL: https://harshith.in/blog/arena-allocators/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: memory, performance, systems

`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

```c
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.
