Cheatsheet: Arena Pattern em Zig

Arena Pattern em Zig

O Arena Pattern agrupa múltiplas alocações em uma única região de memória que é liberada toda de uma vez. Em vez de rastrear e liberar cada alocação individualmente, você aloca livremente durante uma fase de trabalho e libera tudo no final. Em Zig, std.heap.ArenaAllocator implementa esse padrão nativamente.

Quando Usar

  • Processamento de requisições (alocar para processar, liberar ao finalizar)
  • Parsing e compilação (alocar AST, liberar após gerar código)
  • Operações batch com tempo de vida definido
  • Game frames (alocar entidades temporárias por frame)
  • Qualquer operação com fase de alocação seguida de liberação total

Implementação com ArenaAllocator

const std = @import("std");

pub fn main() !void {
    // Allocator pai (quem realmente gerencia a memória)
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    // Arena: aloca rápido, libera tudo junto
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // libera TUDO de uma vez

    const alloc = arena.allocator();

    // Alocar livremente — sem precisar de free individual!
    const nome = try alloc.dupe(u8, "Zig Brasil");
    const numeros = try alloc.alloc(i32, 100);
    const buffer = try alloc.alloc(u8, 4096);

    // Usar os dados...
    _ = nome;
    for (numeros, 0..) |*n, i| n.* = @intCast(i);
    @memset(buffer, 0);

    // Ao final: arena.deinit() libera TUDO
    // Não precisa de defer alloc.free() para cada alocação!
}

Arena por Requisição

const std = @import("std");

const Requisicao = struct {
    metodo: []const u8,
    path: []const u8,
    corpo: []const u8,
};

const Resposta = struct {
    status: u16,
    corpo: []const u8,
};

fn processarRequisicao(
    arena_alloc: std.mem.Allocator,
    req: Requisicao,
) !Resposta {
    // Todas alocações temporárias usam a arena
    const path_normalizado = try std.fmt.allocPrint(arena_alloc, "/{s}", .{
        std.mem.trimLeft(u8, req.path, "/"),
    });

    var partes = std.ArrayList([]const u8).init(arena_alloc);
    var iter = std.mem.splitSequence(u8, path_normalizado, "/");
    while (iter.next()) |parte| {
        try partes.append(parte);
    }

    const corpo_resposta = try std.fmt.allocPrint(arena_alloc, "OK: {s} {s}", .{
        req.metodo, path_normalizado,
    });

    return .{
        .status = 200,
        .corpo = corpo_resposta,
    };
    // Nenhum free necessário — arena cuida de tudo
}

fn servidorLoop(parent_alloc: std.mem.Allocator) !void {
    // Simular processamento de requisições
    for (0..1000) |_| {
        // Nova arena para cada requisição
        var arena = std.heap.ArenaAllocator.init(parent_alloc);
        defer arena.deinit(); // limpa tudo ao final da requisição

        const req = Requisicao{
            .metodo = "GET",
            .path = "/api/dados",
            .corpo = "",
        };

        const resp = try processarRequisicao(arena.allocator(), req);
        _ = resp;
        // arena.deinit() limpa tudo — zero leaks, zero fragmentação
    }
}

Arena com Reset (Reutilização)

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    // Processar múltiplos batches reutilizando memória
    for (0..100) |batch| {
        const alloc = arena.allocator();

        // Alocar para este batch
        const dados = try alloc.alloc(u8, 1024);
        _ = dados;

        const resultado = try std.fmt.allocPrint(alloc, "Batch {d} processado", .{batch});
        _ = resultado;

        // Resetar arena — libera tudo mas MANTÉM a memória alocada do SO
        _ = arena.reset(.retain_capacity);
        // Próximo batch reutiliza a mesma memória — sem novas syscalls
    }
}

Arena sobre Buffer Estático

const std = @import("std");

pub fn main() !void {
    // Arena sobre buffer da stack — zero heap allocations!
    var buffer: [64 * 1024]u8 = undefined; // 64KB na stack
    var fba = std.heap.FixedBufferAllocator.init(&buffer);

    var arena = std.heap.ArenaAllocator.init(fba.allocator());
    defer arena.deinit();

    const alloc = arena.allocator();

    // Alocações vêm do buffer na stack
    const dados = try alloc.alloc(u8, 1024);
    _ = dados;
}

Comparação: Sem Arena vs Com Arena

// SEM arena — muitos defers, fácil esquecer um
fn semArena(allocator: std.mem.Allocator) !void {
    const a = try allocator.alloc(u8, 100);
    defer allocator.free(a);

    const b = try allocator.alloc(u8, 200);
    defer allocator.free(b);

    const c = try allocator.alloc(u8, 300);
    defer allocator.free(c);

    // Cada alocação precisa de seu próprio free
}

// COM arena — um único defer limpa tudo
fn comArena(parent: std.mem.Allocator) !void {
    var arena = std.heap.ArenaAllocator.init(parent);
    defer arena.deinit(); // UM defer para tudo

    const alloc = arena.allocator();
    const a = try alloc.alloc(u8, 100);
    const b = try alloc.alloc(u8, 200);
    const c = try alloc.alloc(u8, 300);
    _ = a; _ = b; _ = c;
    // Simples, limpo, sem risco de leak
}

Quando Evitar

  • Alocações que precisam sobreviver além do escopo da arena
  • Quando objetos têm tempos de vida muito diferentes
  • Quando é preciso liberar objetos individualmente (use GPA)
  • Objetos enormes que desperdiçariam memória se mantidos até o final

Veja Também

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.