Overview
ArenaAllocator is a memory allocation pattern that allows you to allocate many objects and free them all at once. It wraps a child allocator and provides fast bump-pointer allocation.
Key benefits:
Extremely fast allocation (O(1) bump pointer)
Free everything with a single call
Individual frees are no-ops (except last allocation)
Reduced memory management overhead
Basic Usage
const std = @import ( "std" );
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
const allocator = arena . allocator ();
// Allocate many objects
const a = try allocator . alloc ( u8 , 100 );
const b = try allocator . alloc ( u8 , 200 );
const c = try allocator . create ( MyStruct );
// No individual frees needed
// All memory freed when arena.deinit() is called
Structure
pub const ArenaAllocator = struct {
child_allocator : Allocator ,
state : State ,
};
The underlying allocator used to request large buffers.
Internal state tracking allocated buffers and current position.
State
pub const State = struct {
buffer_list : std . SinglyLinkedList = .{},
end_index : usize = 0 ,
};
The State can be stored separately to save memory if you don’t need the full ArenaAllocator.
Methods
init
pub fn init ( child_allocator : Allocator ) ArenaAllocator
Creates a new arena with the specified backing allocator.
Allocator to use for obtaining large memory buffers.
Example:
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
allocator
pub fn allocator ( self : * ArenaAllocator ) Allocator
Returns the standard Allocator interface.
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
const allocator = arena . allocator ();
const data = try allocator . alloc ( u8 , 1024 );
deinit
pub fn deinit ( self : ArenaAllocator ) void
Frees all allocated memory and destroys the arena.
After calling deinit(), all pointers obtained from this arena are invalid.
Example:
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit (); // Clean up everything
const allocator = arena . allocator ();
const list = try allocator . alloc ( i32 , 100 );
// list becomes invalid after arena.deinit()
reset
pub fn reset ( self : * ArenaAllocator , mode : ResetMode ) bool
Resets the arena, making all previously allocated memory available for reuse.
Controls how memory is handled during reset.
true if reset succeeded, false if memory preheating failed (arena still functional).
ResetMode
pub const ResetMode = union ( enum ) {
/// Release all allocated memory back to child allocator
free_all ,
/// Keep allocated memory for reuse (preheat optimization)
retain_capacity ,
/// Keep memory up to specified limit
retain_with_limit : usize ,
};
Examples:
// Free everything
_ = arena . reset (. free_all );
// Keep all allocated capacity
_ = arena . reset (. retain_capacity );
// Keep up to 1MB
_ = arena . reset (.{ . retain_with_limit = 1024 * 1024 });
queryCapacity
pub fn queryCapacity ( self : ArenaAllocator ) usize
Returns total allocated capacity (excluding internal metadata).
Total bytes of usable memory allocated from child allocator.
Example:
const capacity = arena . queryCapacity ();
std . debug . print ( "Arena capacity: {} bytes \n " , .{ capacity });
Usage Patterns
Request Processing
fn handleRequest ( request : Request ) ! Response {
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
const allocator = arena . allocator ();
// All allocations during request processing
const parsed = try parseRequest ( allocator , request );
const result = try processData ( allocator , parsed );
const response = try buildResponse ( allocator , result );
return response ;
// All temporary allocations freed automatically
}
Loop with Reset
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
for ( items ) | item | {
defer _ = arena . reset (. retain_capacity );
const allocator = arena . allocator ();
// Process item with temporary allocations
const temp = try allocator . alloc ( u8 , item . size );
try processItem ( allocator , item , temp );
// Memory reused in next iteration
}
Parser/Compiler
pub const Parser = struct {
arena : std . heap . ArenaAllocator ,
pub fn init ( allocator : Allocator ) Parser {
return .{
. arena = std . heap . ArenaAllocator . init ( allocator ),
};
}
pub fn deinit ( self : * Parser ) void {
self . arena . deinit ();
}
pub fn parse ( self : * Parser , source : [] const u8 ) !* Ast {
const allocator = self . arena . allocator ();
const tokens = try tokenize ( allocator , source );
const ast = try buildAst ( allocator , tokens );
return ast ;
// AST and tokens live until Parser.deinit()
}
};
How It Works
ArenaAllocator uses a bump pointer allocation strategy:
Initial state : No buffers allocated
First allocation : Requests a large buffer from child allocator
Subsequent allocations : Bump pointer forward in current buffer
Buffer full : Allocate new buffer (1.5x previous size)
Free : No-op unless it’s the most recent allocation
Deinit/Reset : Free all buffers at once
Memory Layout
Buffer 1: [allocated | allocated | free space]
↑ ↑ ↑
obj1 obj2 end_index
Buffer 2: [allocated | allocated | allocated | free space]
↑ ↑ ↑ ↑
obj3 obj4 obj5 end_index (current)
Growth Strategy
When a buffer fills up:
const new_size = previous_size + ( previous_size / 2 );
// Grows: 64KB → 96KB → 144KB → 216KB → ...
Bump pointer allocation is extremely fast: // Pseudo-code
fn alloc ( size ) {
const addr = current_buffer + end_index ;
end_index += size ;
return addr ;
}
Typical cost : 1-2 ns per allocation
Individual frees do nothing (except last allocation): allocator . free ( data ); // No-op unless most recent
This makes arena ideal when you don’t need individual frees.
Frees all buffers in linked list: arena . deinit (); // Frees ~10s of buffers
Very fast compared to freeing thousands of individual allocations.
Reset with retain: O(n) buffers
Keeps largest buffer, frees others: _ = arena . reset (. retain_capacity );
Optimization : After first iteration, often only one buffer remains.
Advantages
✅ Fast allocation - Bump pointer is 10-100x faster than general allocators
✅ Simple cleanup - One deinit() call frees everything
✅ Reduced fragmentation - Large buffers reduce memory fragmentation
✅ Cache friendly - Sequential allocations improve locality
✅ No bookkeeping - Don’t track individual allocations
Limitations
❌ No individual frees - Can’t free specific allocations (except last one)
❌ Memory held until deinit - All memory consumed until arena is destroyed
❌ Growing memory use - Long-running operations accumulate memory
❌ Not thread-safe - Requires external synchronization
When to Use
Good use cases:
Request/response handling
Parsing and compilation
Short-lived computations
Building data structures with same lifetime
Rapid prototyping
Bad use cases:
Long-running servers (use per-request arenas)
When individual deallocation is needed
Memory-constrained environments
Concurrent allocation (use ThreadSafeAllocator wrapper)
Advanced: State Management
You can store only the State to save memory:
pub const Parser = struct {
arena_state : std . heap . ArenaAllocator . State = .{},
fn parse ( self : * Parser , allocator : Allocator , source : [] const u8 ) !* Ast {
var arena = self . arena_state . promote ( allocator );
defer self . arena_state = arena . state ;
defer arena . deinit ();
const arena_allocator = arena . allocator ();
// Use arena_allocator for parsing
}
};
Reset Preheating Optimization
When using .retain_capacity, the arena learns your allocation pattern:
var arena = std . heap . ArenaAllocator . init ( std . heap . page_allocator );
defer arena . deinit ();
for ( 0 .. 1000 ) | i | {
defer _ = arena . reset (. retain_capacity );
const allocator = arena . allocator ();
// Allocate ~500KB each iteration
const data = try allocator . alloc ( u8 , 500_000 );
processData ( data );
// After first few iterations:
// - Single 500KB buffer allocated
// - No more child allocator calls
// - Allocation becomes O(1) pointer bump
}
Result : Amortized O(1) complexity with zero allocator calls after warmup.
Testing with Arena
test "my function" {
var arena = std . heap . ArenaAllocator . init ( std . testing . allocator );
defer arena . deinit ();
const allocator = arena . allocator ();
// No need to track individual allocations
const a = try myFunction ( allocator );
const b = try otherFunction ( allocator );
try std . testing . expectEqual ( expected , a . value );
// arena.deinit() cleans up everything
}
See Also