Skip to main content

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,
};
child_allocator
Allocator
The underlying allocator used to request large buffers.
state
State
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.
child_allocator
Allocator
required
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.
mode
ResetMode
required
Controls how memory is handled during reset.
return
bool
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).
return
usize
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:
  1. Initial state: No buffers allocated
  2. First allocation: Requests a large buffer from child allocator
  3. Subsequent allocations: Bump pointer forward in current buffer
  4. Buffer full: Allocate new buffer (1.5x previous size)
  5. Free: No-op unless it’s the most recent allocation
  6. 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 → ...

Performance Characteristics

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

Build docs developers (and LLMs) love