Map

Arena allocation

Wiki conceptmemory-managementrustsystems-programmingperformance โ†ณ show in map Markdown
title
Arena allocation
type
concept
summary
Region-based memory management โ€” allocate many objects into one buffer, hand out references or indices, free the whole region at once
tags
memory-management, rust, systems-programming, performance
created
2026-07-18
updated
2026-07-18

An arena (also called a region, a pool, or a bump allocator) is a block of memory that hands out many small allocations and frees them all together. Instead of calling malloc/free per object, you allocate objects into the arena as you build them, use them for as long as the arena is alive, and drop the entire region in one operation when you're done. The individual objects are never freed on their own.

The idea trades granularity for speed and simplicity. You give up the ability to free one object early, and in return you get very cheap allocation (often just bumping a pointer) and a single, cheap deallocation. It fits any workload with a clear phase boundary: parse a file into an AST, walk it, throw the whole AST away; render a frame, free everything at frame end; handle one request, reset the arena for the next one.

How allocation works

The simplest arena is a bump allocator. It holds a buffer and an offset. Allocating n bytes returns the current offset and advances it by n. There is no free list, no size classes, no coalescing โ€” allocation is a pointer add and a bounds check. When the buffer fills, the arena grabs another chunk (arenas are usually a linked list of chunks so that existing references stay valid; a Vec-backed arena that reallocates would invalidate them). Freeing means dropping every chunk, or resetting the offset to zero to reuse the space.

Because objects sharing an arena are laid out consecutively in memory, walking them tends to be cache-friendly โ€” a traversal touches nearby addresses instead of chasing pointers scattered across the heap. That locality is often a bigger win than the cheaper allocation itself.

References vs. indices

There are two common ways to refer to something stored in an arena, and the choice has real consequences.

Reference-handing arenas give you back a real pointer (&T) into the region. In Rust the typed_arena crate works this way: arena.alloc(value) returns &mut T, and the borrow checker ties that reference's lifetime to the arena's, so you cannot hold a reference after the arena is dropped. This is ergonomic โ€” you can pattern-match through the references directly, which Rust otherwise can't do across a Box in a recursive enum โ€” but each reference is a full pointer (64 bits on most targets), and a whole tree of them shares one arena lifetime that then propagates through every function signature that touches it.

Index-handing arenas (the "generational arena" family, e.g. generational-arena or slotmap) store objects in a backing Vec and hand out an integer index instead of a pointer. To dereference, you index back into the arena. Indices can be 32 or even 16 bits, so they pack more tightly than pointers โ€” which matters when you have many small nodes. The "generational" part adds a generation counter to each slot so a stale index (pointing at a slot that was reused) can be detected instead of silently reading the wrong object.

Why index-not-pointer sidesteps borrow-checker fights

The index approach is popular in Rust specifically because it dodges the borrow checker. A graph or doubly-linked structure built from &T references forces you to prove to the compiler that every reference outlives every use and that aliasing rules hold โ€” which for cyclic or self-referential structures is often impossible without unsafe, Rc<RefCell<T>>, or Pin. If nodes instead live in a Vec<Node> and edges are usize indices, the references the borrow checker cares about are just the short-lived &self.nodes[i] you take at the moment of access. The graph's own structure carries no lifetimes. This is the same move safe-gc makes: route every access through indexing into a Heap rather than through a raw pointer, and whole categories of use-after-free and aliasing bugs leave the reachable state space. The cost is that a stale or wrong index is a logic bug rather than a compile error โ€” generation counters exist to turn that back into a detectable runtime failure.

Interning

An arena pairs naturally with interning: allocate-and-deduplicate. Before storing a value, hash it and check whether an equal one already lives in the arena; if so, hand back the existing handle. Now equal values share one allocation and equality is a handle comparison instead of a deep compare. rustc does this heavily โ€” it interns types into an arena so that identical types are pointer-equal, which makes both type comparison and caching cheap, and it interns the HIR during AST lowering, handing out HirId handles. The Gleam pretty printer change below uses a lighter version: keywords and separators like String("fn") or the list comma are allocated once into the arena and referenced everywhere instead of being reboxed each time.

Lifetime and deallocation

The defining constraint is that you cannot free one object early. Everything in the arena dies together. For phase-structured work this is a feature โ€” you never leak and never double-free within a phase, and cleanup is O(1) chunks rather than O(n) objects. For long-lived structures with genuinely independent object lifetimes it's the wrong tool: an arena that only grows will hold memory until the phase ends, so a server that arena-allocates per connection must reset the arena between requests or it behaves like a leak. This is the opposite discipline from per-object schemes like raii, where each object's destructor runs deterministically at its own scope exit, and from reclamation schemes like epoch-based-reclamation that exist precisely to free individual nodes safely under concurrency.

Example: the Gleam pretty printer

gleam-pretty-printer-arenas is a concrete case. Gleam's formatter builds a recursive Document tree where nested variants were boxed on the heap (Nest(Box<Self>)). Switching those Boxes to arena references (Nest(&'doc Self)) backed by typed_arena removed most of the per-node heap allocation and let repeated documents be cached once. The formatter got about 24% faster and peak memory dropped roughly 10%. The cost was mechanical: every function that built documents had to take the arena as an extra argument and thread its lifetime through.

Arenas show up across the language-implementation and systems world for the same reasons โ€” bump allocation is fast, bulk free is trivial, and a manual-memory language like those in dayvster-manual-memory-management gets much of the safety benefit of automatic management within a bounded region. In concurrent settings, sharing an arena across threads runs into the usual rust-send-sync constraints: a single-threaded bump arena is neither Send nor Sync by default, and making one safe to share requires either synchronization or per-thread arenas.