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.