Arena Allocator em Zig: Guia Prático com Exemplos Reais

O Arena Allocator é provavelmente o padrão de alocação mais poderoso em Zig. No artigo anterior apresentamos brevemente os allocators. Agora vamos explorar o Arena Allocator em profundidade, com exemplos práticos que você pode aplicar imediatamente nos seus projetos.

O Que é um Arena Allocator?

Um Arena Allocator agrupa múltiplas alocações em blocos grandes e as libera todas de uma vez. Em vez de rastrear cada alocação individual, você simplesmente descarta a arena inteira quando terminar.

Pense nisso como um quadro branco: você escreve o quanto quiser, e quando termina, apaga tudo de uma vez — sem precisar apagar cada palavra individualmente.

const std = @import("std");

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

    // Criar arena com backing allocator
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // Uma única chamada libera TUDO

    const alloc = arena.allocator();

    // Muitas alocações — nenhum defer individual necessário
    const nome = try alloc.alloc(u8, 100);
    const sobrenome = try alloc.alloc(u8, 100);
    const numeros = try alloc.alloc(i32, 50);
    const buffer = try alloc.alloc(u8, 4096);

    @memcpy(nome[0..3], "Zig");
    @memcpy(sobrenome[0..6], "Brasil");
    numeros[0] = 42;
    buffer[0] = 0xFF;

    std.debug.print("Tudo alocado sem nenhum defer individual!\n", .{});
    std.debug.print("Nome: {s}, Sobrenome: {s}\n", .{ nome[0..3], sobrenome[0..6] });
    // arena.deinit() no defer libera nome, sobrenome, numeros e buffer
}

Caso de Uso 1: Processamento de Requisições HTTP

O exemplo clássico de arena allocator é o processamento de requisições HTTP. Cada requisição aloca memória temporária, e tudo é liberado quando a requisição termina.

const std = @import("std");

const HttpRequest = struct {
    method: []const u8,
    path: []const u8,
    headers: std.StringHashMap([]const u8),
    body: []const u8,
};

const HttpResponse = struct {
    status: u16,
    body: []const u8,
};

fn parseRequest(allocator: std.mem.Allocator, raw: []const u8) !HttpRequest {
    var headers = std.StringHashMap([]const u8).init(allocator);

    // Simular parsing — todas as alocações usam o allocator da arena
    const method = try allocator.alloc(u8, 3);
    @memcpy(method, "GET");

    const path = try std.fmt.allocPrint(allocator, "/api/users", .{});

    try headers.put(
        try std.fmt.allocPrint(allocator, "Content-Type", .{}),
        try std.fmt.allocPrint(allocator, "application/json", .{}),
    );

    _ = raw;

    return HttpRequest{
        .method = method,
        .path = path,
        .headers = headers,
        .body = "",
    };
}

fn handleRequest(allocator: std.mem.Allocator, request: HttpRequest) !HttpResponse {
    const body = try std.fmt.allocPrint(
        allocator,
        "{{\"message\": \"Olá do {s} {s}\"}}",
        .{ request.method, request.path },
    );

    return HttpResponse{
        .status = 200,
        .body = body,
    };
}

fn processarRequisicao(backing_allocator: std.mem.Allocator, raw_request: []const u8) !void {
    // Arena para esta requisição — tudo é liberado ao final
    var arena = std.heap.ArenaAllocator.init(backing_allocator);
    defer arena.deinit();

    const alloc = arena.allocator();

    const request = try parseRequest(alloc, raw_request);
    const response = try handleRequest(alloc, request);

    std.debug.print("Status: {d}\n", .{response.status});
    std.debug.print("Body: {s}\n", .{response.body});

    // Ao sair desta função, arena.deinit() libera TODA a memória
    // de parsing, headers, body de resposta — tudo de uma vez
}

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

    // Simular várias requisições
    for (0..3) |i| {
        std.debug.print("\n--- Requisição {d} ---\n", .{i + 1});
        try processarRequisicao(gpa.allocator(), "GET /api/users HTTP/1.1\r\n");
    }
}

Cada requisição cria sua própria arena e, ao terminar, toda a memória é liberada com uma única operação. Isso é mais rápido do que liberar cada alocação individualmente e elimina a possibilidade de memory leaks por alocações esquecidas.

Caso de Uso 2: Parser de Configuração

Parsers são outro caso ideal para arenas, pois geram muitas strings e estruturas intermediárias:

const std = @import("std");

const ConfigValue = union(enum) {
    string: []const u8,
    integer: i64,
    boolean: bool,
};

const Config = struct {
    valores: std.StringHashMap(ConfigValue),
    allocator: std.mem.Allocator,

    fn init(allocator: std.mem.Allocator) Config {
        return .{
            .valores = std.StringHashMap(ConfigValue).init(allocator),
            .allocator = allocator,
        };
    }

    fn set(self: *Config, chave: []const u8, valor: ConfigValue) !void {
        const chave_copia = try self.allocator.alloc(u8, chave.len);
        @memcpy(chave_copia, chave);
        try self.valores.put(chave_copia, valor);
    }

    fn getString(self: *const Config, chave: []const u8) ?[]const u8 {
        if (self.valores.get(chave)) |val| {
            switch (val) {
                .string => |s| return s,
                else => return null,
            }
        }
        return null;
    }

    fn getInt(self: *const Config, chave: []const u8) ?i64 {
        if (self.valores.get(chave)) |val| {
            switch (val) {
                .integer => |i| return i,
                else => return null,
            }
        }
        return null;
    }
};

fn parseConfig(allocator: std.mem.Allocator, fonte: []const u8) !Config {
    var config = Config.init(allocator);

    // Simular parsing de um arquivo de configuração
    _ = fonte;

    const host = try std.fmt.allocPrint(allocator, "localhost", .{});
    try config.set("host", .{ .string = host });
    try config.set("port", .{ .integer = 8080 });
    try config.set("debug", .{ .boolean = true });

    const db_url = try std.fmt.allocPrint(allocator, "postgres://localhost:5432/app", .{});
    try config.set("database_url", .{ .string = db_url });

    return config;
}

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

    // Arena para toda a configuração
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();

    var config = try parseConfig(arena.allocator(), "host=localhost\nport=8080\n");

    if (config.getString("host")) |host| {
        std.debug.print("Host: {s}\n", .{host});
    }
    if (config.getInt("port")) |port| {
        std.debug.print("Port: {d}\n", .{port});
    }
    if (config.getString("database_url")) |url| {
        std.debug.print("DB: {s}\n", .{url});
    }
}

Caso de Uso 3: Processamento de Dados em Lote

Quando você processa dados em lotes, pode criar uma arena por lote e resetá-la entre iterações:

const std = @import("std");

const Estatisticas = struct {
    media: f64,
    max: f64,
    min: f64,
    total: f64,
};

fn processarLote(
    allocator: std.mem.Allocator,
    dados_brutos: []const f64,
) !Estatisticas {
    // Copiar e processar dados usando arena
    const dados = try allocator.alloc(f64, dados_brutos.len);
    @memcpy(dados, dados_brutos);

    // Ordenar (precisaria de buffer auxiliar na arena também)
    std.mem.sort(f64, dados, {}, std.sort.asc(f64));

    var total: f64 = 0;
    for (dados) |v| {
        total += v;
    }

    return Estatisticas{
        .media = total / @as(f64, @floatFromInt(dados.len)),
        .max = dados[dados.len - 1],
        .min = dados[0],
        .total = total,
    };
}

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

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

    // Simular vários lotes de dados
    const lotes = [_][]const f64{
        &.{ 3.2, 1.5, 4.7, 2.8, 6.1 },
        &.{ 10.0, 20.0, 15.0, 25.0 },
        &.{ 100.5, 200.3, 50.1, 75.8, 150.2, 300.0 },
    };

    for (lotes, 1..) |lote, i| {
        // Reset da arena entre lotes — libera memória do lote anterior
        _ = arena.reset(.retain_capacity);

        const stats = try processarLote(arena.allocator(), lote);
        std.debug.print("Lote {d}: média={d:.2}, min={d:.2}, max={d:.2}, total={d:.2}\n", .{
            i, stats.media, stats.min, stats.max, stats.total,
        });
    }
}

O reset(.retain_capacity) libera todas as alocações mas mantém a memória reservada, evitando idas ao sistema operacional nos próximos lotes.

Arena Scoped: Padrão de Escopo Temporário

Uma técnica poderosa é criar sub-escopos com arenas temporárias:

const std = @import("std");

fn operacaoCompleta(permanent_alloc: std.mem.Allocator) ![]u8 {
    // Arena temporária para cálculos intermediários
    var temp_arena = std.heap.ArenaAllocator.init(permanent_alloc);
    defer temp_arena.deinit();

    const temp = temp_arena.allocator();

    // Dados intermediários — serão liberados automaticamente
    const buffer1 = try temp.alloc(u8, 1000);
    const buffer2 = try temp.alloc(u8, 2000);
    _ = buffer2;

    for (buffer1, 0..) |*b, i| {
        b.* = @intCast(i % 256);
    }

    // Resultado final — alocado no allocator permanente
    const resultado = try permanent_alloc.alloc(u8, 10);
    @memcpy(resultado, buffer1[0..10]);

    return resultado;
    // temp_arena.deinit() libera buffer1 e buffer2 automaticamente
    // resultado sobrevive porque foi alocado no permanent_alloc
}

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

    const resultado = try operacaoCompleta(gpa.allocator());
    defer gpa.allocator().free(resultado);

    std.debug.print("Resultado: {any}\n", .{resultado});
}

Melhores Práticas com Arena Allocator

  1. Use arena por unidade de trabalho — Uma requisição HTTP, um frame de jogo, um lote de processamento
  2. Prefira reset(.retain_capacity) em loops — Evita syscalls repetidas
  3. Combine com GPA para debugging — Passe GPA como backing allocator durante desenvolvimento
  4. Não libere individualmente — O objetivo da arena é liberar tudo junto; free() individual é no-op

Resumo

PadrãoQuando Usar
Arena por requisiçãoServidores HTTP, processamento de mensagens
Arena por lotePipeline de dados, ETL
Arena temporáriaCálculos intermediários dentro de uma operação
Arena com resetLoops de processamento com padrão repetitivo

No próximo artigo, vamos aprender a criar custom allocators para necessidades específicas da sua aplicação.

Leitura Complementar

Continue aprendendo Zig

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