Free Everything at Once: The Arena Allocator

malloc and free solve a hard problem: hand out and reclaim memory blocks of any size, in any order, forever, without fragmenting into uselessness. That generality has a price. Each allocation searches free lists and updates bookkeeping, each free has to be matched exactly once (miss one and you leak, do it twice and you corrupt the heap), and the metadata and fragmentation cost real memory.
Here is the thing: a lot of code does not need that generality. A compiler builds an AST for one source file, then discards the whole tree. A game allocates a swarm of objects for one frame, then resets for the next. A web server allocates scratch data for one request, then drops all of it when the response is sent. In every case the objects share a lifetime: they are all born together and all die together. For that pattern, general-purpose allocation is a slow, leak-prone overkill, and an arena is the right tool.
Allocation is moving a pointer
An arena (also called a bump or region allocator) is a big block of memory and an offset into it. To allocate, you round the offset up for alignment, hand back the current position, and bump the offset forward. That is the entire allocation path.
typedef struct {
unsigned char *buf;
size_t cap;
size_t off; // next free offset
} Arena;
void *arena_alloc(Arena *a, size_t size, size_t align) {
size_t aligned = (a->off + (align - 1)) & ~(align - 1); // round up to alignment
if (aligned > a->cap || size > a->cap - aligned) {
return NULL; // out of room, checked without overflow
}
void *p = a->buf + aligned;
a->off = aligned + size;
return p;
}
No free list, no search, no per-object header. An allocation is a handful of arithmetic operations and a pointer return. It is about as fast as memory allocation gets, and it is trivially thread-safe to make (one atomic add) if you ever need it to be.
Two details earn their keep:
Alignment. (off + align - 1) & ~(align - 1) rounds the offset up to the next multiple of align (which must be a power of two). This is the same bit trick allocators everywhere use to satisfy a type's alignment requirement, because a misaligned double or pointer is a crash on some architectures and a slowdown on others.
The overflow-safe bounds check. The naive check aligned + size > cap can wrap around if size is enormous and lie that there is room. Writing it as size > cap - aligned (after confirming aligned <= cap) compares within range and cannot overflow. In C, the careful version of an arithmetic check is not optional; it is the difference between a guard and a vulnerability.
Freeing is forgetting
There is no arena_free for individual objects, and that is the point. You free the entire arena at once by resetting the offset:
void arena_reset(Arena *a) { a->off = 0; }
One assignment reclaims everything. No walking a tree to free each node, no matching every allocation with a deallocation, no chance of leaking one object or double-freeing another. The whole class of use-after-free and double-free bugs simply cannot occur for arena objects, because there is no per-object free to misuse. You allocate freely during the frame, the request, the parse, then wipe the slate in O(1).
Where this shows up
- Compilers and interpreters allocate AST and IR nodes in arenas tied to a compilation unit, then drop the arena. (This is core to how clang and many language runtimes manage memory.)
- Game engines keep a per-frame arena: allocate transient objects during the frame,
resetat the end. Zero fragmentation, zero GC pauses, predictable cost. - Servers give each request an arena for scratch allocations, freed when the request finishes. This is the heart of what makes some high-throughput servers fast and leak-free.
The trade you are making
Arenas are not a free lunch, they are a different lunch. You give up the ability to free individual objects early, so memory can sit unused until the whole arena resets, and an object that needs to outlive the arena has to be copied out. In exchange you get allocation that is nearly free, deallocation that is a single instruction, and a whole category of memory bugs designed out of existence.
The broader idea, region-based memory management, is old and powerful: tie a group of allocations to a lifetime and manage them as one. When your objects already share a lifetime, stop fighting malloc for the privilege of freeing them one at a time, and just bump a pointer.
Build it yourself. Solve the Arena Allocator challenge on barehands and get graded on correctness and speed. No libraries, just cc -std=c11.