Tipos de Allocators em Zig: Guia Completo para Escolher o Allocator Certo

No artigo anterior, exploramos os fundamentos de stack e heap. Agora é hora de mergulhar no coração do gerenciamento de memória em Zig: os allocators. Zig oferece uma abordagem única onde todo allocator segue a mesma interface std.mem.Allocator, permitindo que seu código seja flexível e testável.

A Interface std.mem.Allocator

Antes de explorar os tipos específicos, entenda que todo allocator em Zig implementa a mesma interface. Isso significa que você pode trocar allocators sem modificar a lógica do seu código.

const std = @import("std");

// Qualquer função que precisa alocar memória recebe um Allocator
fn criarMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
    return std.fmt.allocPrint(allocator, "Olá, {s}! Bem-vindo ao Zig.", .{nome});
}

pub fn main() !void {
    // Podemos usar QUALQUER allocator aqui — a função não se importa
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const msg = try criarMensagem(allocator, "Brasil");
    defer allocator.free(msg);

    std.debug.print("{s}\n", .{msg});
}

As operações fundamentais da interface são:

OperaçãoDescrição
alloc(T, n)Aloca array de n elementos do tipo T
create(T)Aloca um único valor do tipo T
free(slice)Libera memória alocada com alloc
destroy(ptr)Libera memória alocada com create
realloc(slice, new_n)Redimensiona uma alocação existente

1. page_allocator — O Mais Básico

O page_allocator aloca memória diretamente do sistema operacional, em páginas (geralmente 4 KB). É o allocator mais simples, mas também o menos eficiente para alocações pequenas.

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    // Aloca diretamente do SO — mínimo uma página (4096 bytes)
    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);

    std.debug.print("Solicitado: 100 bytes\n", .{});
    std.debug.print("Endereço: {*}\n", .{dados.ptr});
    std.debug.print("Tamanho real do slice: {d}\n", .{dados.len});

    // Note: mesmo pedindo 100 bytes, o SO aloca uma página inteira
    // Os 100 bytes retornados são um slice da página completa
}

Quando usar: Alocações grandes e infrequentes, ou como allocator de backing para outros allocators.

Quando evitar: Alocações pequenas e frequentes (desperdiça memória por alocar páginas inteiras).

2. GeneralPurposeAllocator (GPA) — O Canivete Suíço

O GeneralPurposeAllocator é o allocator mais versátil. Ele é otimizado para uso geral e inclui proteções contra bugs de memória.

const std = @import("std");

pub fn main() !void {
    // Configurações do GPA (valores padrão mostrados)
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .stack_trace_frames = 8, // Frames de stack trace para debug
        .safety = true, // Detecta use-after-free, double-free
        // .never_unmap = false, // Útil para debugging avançado
        // .retain_metadata = false, // Manter metadados após free
    }){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("ERRO: Memory leak detectado!\n", .{});
        }
    }
    const allocator = gpa.allocator();

    // Uso normal
    const lista = try allocator.alloc(i32, 10);
    for (lista, 0..) |*item, i| {
        item.* = @intCast(i * 10);
    }

    // Demonstrar realloc
    const lista_maior = try allocator.realloc(lista, 20);
    for (lista_maior[10..], 10..) |*item, i| {
        item.* = @intCast(i * 10);
    }
    defer allocator.free(lista_maior);

    std.debug.print("Lista com {d} elementos:\n", .{lista_maior.len});
    for (lista_maior) |item| {
        std.debug.print("  {d}\n", .{item});
    }
}

O GPA detecta automaticamente:

  • Memory leaks — ao chamar deinit(), reporta memória não liberada
  • Use-after-free — acessar memória já liberada
  • Double-free — liberar a mesma memória duas vezes
  • Buffer overflow — escrever além dos limites alocados

Quando usar: Desenvolvimento, debugging, e quando não tem certeza de qual allocator escolher.

Quando evitar: Código de altíssima performance onde cada nanosegundo conta (raro).

3. FixedBufferAllocator — Alocação sem Heap

O FixedBufferAllocator usa um buffer pré-existente (que pode estar na stack) como fonte de memória. Não faz nenhuma chamada ao sistema operacional.

const std = @import("std");

pub fn main() !void {
    // Buffer na stack — sem alocação de heap
    var buffer: [4096]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buffer);
    const allocator = fba.allocator();

    // Aloca dentro do buffer fixo
    const nome = try allocator.alloc(u8, 20);
    @memcpy(nome[0..9], "Zig Legal");

    const numeros = try allocator.alloc(i32, 5);
    for (numeros, 0..) |*n, i| {
        n.* = @intCast(i + 1);
    }

    std.debug.print("Nome: {s}\n", .{nome[0..9]});
    std.debug.print("Números: ", .{});
    for (numeros) |n| {
        std.debug.print("{d} ", .{n});
    }
    std.debug.print("\n", .{});

    // Podemos resetar e reutilizar todo o buffer
    fba.reset();
    std.debug.print("Buffer resetado — {d} bytes disponíveis novamente\n", .{buffer.len});

    // Nova alocação começa do início do buffer
    const novo = try allocator.alloc(u8, 100);
    std.debug.print("Nova alocação de {d} bytes após reset\n", .{novo.len});
}

Quando usar: Sistemas embarcados, contextos sem heap, ou quando o tamanho máximo de memória é conhecido.

Quando evitar: Quando o tamanho total das alocações é imprevisível.

4. ArenaAllocator — Alocação em Bloco

O ArenaAllocator agrupa muitas alocações pequenas e as libera todas de uma vez. É extremamente eficiente para padrões de “alocar muito, liberar tudo junto”.

const std = @import("std");

const Registro = struct {
    nome: []const u8,
    idade: u32,
};

fn processarDados(allocator: std.mem.Allocator) !void {
    const nomes = [_][]const u8{ "Ana", "Bruno", "Carla", "Diego", "Elena" };
    var registros = std.ArrayList(Registro).init(allocator);
    defer registros.deinit();

    for (nomes, 25..) |nome, idade| {
        // Cada alocação individual não precisa de defer
        const nome_copia = try allocator.alloc(u8, nome.len);
        @memcpy(nome_copia, nome);

        try registros.append(.{
            .nome = nome_copia,
            .idade = @intCast(idade),
        });
    }

    for (registros.items) |reg| {
        std.debug.print("{s} ({d} anos)\n", .{ reg.nome, reg.idade });
    }
}

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

    // Arena usa GPA como backing allocator
    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit(); // Libera TUDO de uma vez

    // Todas as alocações dentro de processarDados são na arena
    try processarDados(arena.allocator());

    std.debug.print("\nTodas as alocações liberadas de uma vez com arena.deinit()\n", .{});
}

Vamos explorar o ArenaAllocator em profundidade no próximo artigo.

5. Comparação Prática: Benchmark de Allocators

Vamos comparar o desempenho dos allocators em um cenário real:

const std = @import("std");

fn benchmarkAllocator(
    allocator: std.mem.Allocator,
    nome: []const u8,
    n_alocacoes: usize,
) !void {
    var timer = try std.time.Timer.start();

    var ponteiros = std.ArrayList([]u8).init(std.heap.page_allocator);
    defer ponteiros.deinit();

    // Fase 1: Alocar
    for (0..n_alocacoes) |i| {
        const tamanho = (i % 256) + 1;
        const ptr = try allocator.alloc(u8, tamanho);
        try ponteiros.append(ptr);
    }

    // Fase 2: Liberar
    for (ponteiros.items) |ptr| {
        allocator.free(ptr);
    }

    const elapsed = timer.read();
    std.debug.print("{s}: {d} alocações em {d}ms\n", .{
        nome,
        n_alocacoes,
        elapsed / std.time.ns_per_ms,
    });
}

pub fn main() !void {
    const N = 10_000;

    // Benchmark page_allocator
    try benchmarkAllocator(std.heap.page_allocator, "page_allocator", N);

    // Benchmark GPA
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    try benchmarkAllocator(gpa.allocator(), "GPA", N);

    // Benchmark FixedBufferAllocator
    const buf = try std.heap.page_allocator.alloc(u8, 1024 * 1024 * 10);
    defer std.heap.page_allocator.free(buf);
    var fba = std.heap.FixedBufferAllocator.init(buf);
    try benchmarkAllocator(fba.allocator(), "FixedBuffer", N);
}

Tabela Resumo: Qual Allocator Usar?

AllocatorMelhor ParaEvitar QuandoPerformance
page_allocatorBacking de outros allocatorsAlocações pequenasBaixa
GPADesenvolvimento e debugHot paths extremosMédia
FixedBufferAllocatorEmbarcados, sem heapTamanho imprevisívelAlta
ArenaAllocatorAlocações temporárias em loteLiberação individualMuito alta

Padrão: Injeção de Allocator

O padrão mais importante em Zig é nunca hard-code um allocator. Sempre receba-o como parâmetro:

const std = @import("std");

const MinhaStruct = struct {
    allocator: std.mem.Allocator,
    dados: std.ArrayList(u8),

    fn init(allocator: std.mem.Allocator) MinhaStruct {
        return .{
            .allocator = allocator,
            .dados = std.ArrayList(u8).init(allocator),
        };
    }

    fn deinit(self: *MinhaStruct) void {
        self.dados.deinit();
    }

    fn adicionar(self: *MinhaStruct, valor: u8) !void {
        try self.dados.append(valor);
    }
};

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

    var s = MinhaStruct.init(gpa.allocator());
    defer s.deinit();

    try s.adicionar(42);
    try s.adicionar(100);

    std.debug.print("Dados: {any}\n", .{s.dados.items});
}

Este padrão permite que você use GPA em desenvolvimento (para detectar leaks) e troque para um allocator mais eficiente em produção — sem mudar nenhuma linha de código.

Próximos Passos

Agora que você conhece os tipos de allocators, no próximo artigo vamos mergulhar fundo no Arena Allocator na prática — entendendo seus casos de uso avançados e padrões de design.

Referências

Continue aprendendo Zig

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