🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Arena Strategies

An arena strategy is a plan for how long an arena lives and when it is reset. Short-lived arenas keep temporary memory tidy.

One arena per phase

Each loop round gets a fresh arena. The arena deinit at the end of the round frees that round's memory.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    for (0..2) |phase| {
        var arena = std.heap.ArenaAllocator.init(gpa.allocator());
        defer arena.deinit();
        const buf = try arena.allocator().alloc(u8, 1);
        buf[0] = @intCast(phase + 1);
        try out.print("{d}\n", .{buf[0]});
    }
}

A child arena

The child arena holds temporary data. The parent value is still readable after the child arena is destroyed.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();
    const config = try arena.allocator().alloc(u8, 1);
    config[0] = 9;
    {
        var child = std.heap.ArenaAllocator.init(arena.allocator());
        defer child.deinit();
        const temp = try child.allocator().alloc(u8, 1);
        temp[0] = 5;
        try out.print("{d}\n", .{temp[0]});
    }
    try out.print("{d}\n", .{config[0]});
}

Reset between rounds

Resetting with retain_capacity keeps the buffers. Each round recomputes the same sum of 10.

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();
    for (0..3) |_| {
        const buf = try arena.allocator().alloc(u8, 4);
        buf[0] = 1;
        buf[1] = 2;
        buf[2] = 3;
        buf[3] = 4;
        var sum: usize = 0;
        for (buf) |v| sum += v;
        try out.print("{d}\n", .{sum});
        _ = arena.reset(.retain_capacity);
    }
}

Example

const std = @import("std");
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    for (0..2) |req| {
        var arena = std.heap.ArenaAllocator.init(gpa.allocator());
        defer arena.deinit();
        const msg = try std.fmt.allocPrint(arena.allocator(), "request {d} done", .{req});
        try out.print("{s}\n", .{msg});
    }
}

Key points

  • Arena lifetime: an arena lifetime is the span from an arena's creation to its deinit.
  • A per-phase arena is created at the start of a phase and destroyed at its end.
  • A child arena allocates from its parent arena and frees its own blocks at its own deinit.
  • Resetting an arena with retain_capacity keeps its buffers for the next round.
  • Resetting is cheaper than destroying and recreating an arena in a loop.
  • Memory from a destroyed arena must never be read afterwards.

Each loop round creates its own arena. The message is built inside that arena. The arena deinit at the end of the round frees the message. The next round starts with a clean arena.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

Why reset an arena with retain_capacity inside a loop instead of destroying it?