Skip to content

Allocators

Se há um único conceito que faz alguém “começar a pensar em Zig”, é este. A regra de que nenhuma função da biblioteca padrão aloca memória sem receber um allocator parece, à primeira vista, uma inconveniência: por que passar o mesmo argumento a toda função que constrói uma lista ou uma tabela hash? A resposta é que essa inconveniência é a interface pela qual você assume o controle que outras linguagens escondem. A política de memória, isto é, de onde os bytes vêm e quando voltam, deixa de ser uma decisão fixa da linguagem e vira um parâmetro que você troca conforme o contexto.

O allocator é uma interface, não um tipo

Section titled “O allocator é uma interface, não um tipo”

std.mem.Allocator não é uma implementação, é um contrato. Por baixo, é um struct pequeno com dois campos: um ponteiro opaco para o estado do allocator concreto e um ponteiro para um vtable, a tabela de funções que sabem alocar, redimensionar e liberar. É exatamente o padrão de interface à mão que a composição descreve, aplicado à peça mais central da biblioteca padrão:

a-forma-do-allocator.zig
// Forma simplificada de std.mem.Allocator: um ponteiro de estado
// e um vtable, o mesmo par que sustenta qualquer interface manual em Zig.
const Allocator = struct {
ptr: *anyopaque, // estado do allocator concreto (arena, GPA, ...)
vtable: *const VTable, // as funções que operam sobre esse estado
const VTable = struct {
alloc: *const fn (ctx: *anyopaque, len: usize, alinhamento: u8) ?[*]u8,
resize: *const fn (ctx: *anyopaque, buf: []u8, novo_len: usize) bool,
free: *const fn (ctx: *anyopaque, buf: []u8) void,
};
};

Qualquer coisa que preencha esse vtable é um allocator válido, e é por isso que o mesmo código funciona com qualquer estratégia por baixo. Uma função que recebe allocator: std.mem.Allocator não sabe nem se importa se por trás dele há uma arena, um buffer na stack ou o sistema operacional; ela só chama allocator.alloc, allocator.create e allocator.free, e a estratégia concreta decide o que isso significa.

o-padrao.zig
const std = @import("std");
// Note o primeiro parâmetro: a função não escolhe de onde a memória vem,
// quem chama escolhe. Toda a std é escrita assim.
fn duplicar(allocator: std.mem.Allocator, entrada: []const u8) ![]u8 {
const saida = try allocator.alloc(u8, entrada.len);
@memcpy(saida, entrada);
return saida;
}

O try aparece porque alocar pode falhar, e em Zig a falta de memória é um erro comum, um valor devolvido, não uma exceção fatal. Quem chama duplicar decide o que fazer quando a alocação falha, e decide também de qual allocator os bytes saem. Essa dupla decisão, na mão de quem chama, resume a decisão central da linguagem numa assinatura de função.

A biblioteca padrão oferece um punhado de allocators, e a questão é casar cada um com o perfil do problema. Eles não competem; ocupam nichos diferentes, e a grade abaixo serve de consulta rápida antes da prosa explicar o porquê de cada um:

AllocatorDe onde vêm os bytesComo liberaIdeal para
page_allocatordireto do SO, uma página por vezpor alocaçãopoucas alocações grandes; base dos outros
DebugAllocator (ex-GeneralPurposeAllocator)de um pool próprio, vigiadopor alocação; acusa vazamento no deinitdesenvolvimento e testes
ArenaAllocatorde um bloco que crescetudo de uma vez, num deinittempo de vida por fases
FixedBufferAllocatorde um buffer que você já temtudo de uma vez, sem syscallembarcado, caminho quente, teto rígido

O page_allocator é o mais simples e o mais bruto. Ele pede memória direto ao sistema operacional, uma página por vez, e cada alocação é uma syscall. Isso o torna ideal para poucas alocações grandes e péssimo para muitas pequenas: pedir dez bytes por esse caminho custa uma ida ao kernel e devolve uma página inteira de qualquer forma. É o allocator de base sobre o qual outros são construídos.

O GeneralPurposeAllocator (renomeado para DebugAllocator na linha 0.15, com o nome antigo mantido como alias) é o allocator padrão do desenvolvimento. Ele é seguro por construção: em modo debug, detecta vazamentos, uso após liberação e liberação dupla, e aponta a linha culpada. Você paga em desempenho pela vigilância, e por isso troca por um allocator mais enxuto quando compila para produção.

gpa.zig
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const vazou = gpa.deinit();
if (vazou == .leak) std.debug.print("vazamento detectado!\n", .{});
}
const allocator = gpa.allocator();

O ArenaAllocator muda a economia. Em vez de liberar cada alocação individualmente, você aloca à vontade de uma arena e libera tudo de uma só vez, num único deinit. É ideal para trabalho com tempo de vida bem definido: o processamento de uma requisição, uma passada de um parser, a construção de uma árvore que será descartada inteira. Você deixa de rastrear cada free e passa a raciocinar por fases, e o custo de liberar mil nós vira o custo de liberar um.

arena.zig
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // libera tudo que a arena distribuiu, de uma vez
const allocator = arena.allocator();
// Aloque quantos nós quiser sem nunca chamar free individualmente.
const a = try allocator.create(Node);
const b = try allocator.create(Node);
_ = .{ a, b };

O FixedBufferAllocator vai ao extremo oposto do page_allocator: ele distribui memória de um buffer que você já tem, tipicamente um array na própria stack, e nunca faz uma syscall. É um ponteiro que avança sobre bytes pré-reservados, e quando o buffer acaba, a alocação falha com erro. Serve a código que não pode ou não quer tocar o heap: sistemas embarcados, caminhos quentes onde nenhuma ida ao kernel é aceitável, ou testes que querem um teto rígido de memória.

O retorno de todo esse cuidado aparece quando você percebe que suas estruturas de dados herdam a flexibilidade de graça. A std.ArrayListUnmanaged, o vetor dinâmico da linguagem, não guarda um allocator dentro de si; ela recebe um a cada operação que precisa crescer. A mesma lista roda numa arena, num buffer fixo ou no allocator de propósito geral, e a decisão é de quem a usa.

arraylist.zig
var lista: std.ArrayListUnmanaged(u32) = .{};
defer lista.deinit(allocator); // o allocator volta aqui
try lista.append(allocator, 42); // e aqui, a cada crescimento
try lista.append(allocator, 7);
for (lista.items) |x| std.debug.print("{d}\n", .{x});

Os exercícios que consolidam o conceito são os mesmos que o roadmap sugere: escreva um vetor dinâmico, um parser, uma árvore, todos recebendo um allocator. Quando construir a mesma estrutura e testá-la primeiro numa arena, depois num FixedBufferAllocator, sem mudar uma linha da estrutura, o modelo terá virado intuição. É o momento em que “sem alocação escondida” deixa de ser um princípio abstrato e passa a ser prática.

Conceitos: allocator como interface, vtable, ponteiro opaco, política de memória, arena, buffer fixo, vazamento, uso após liberação, tempo de vida por fases Allocators: page_allocator, GeneralPurposeAllocator, DebugAllocator, ArenaAllocator, FixedBufferAllocator Linguagem: std.mem.Allocator, allocator.alloc, allocator.create, allocator.free, try, std.ArrayListUnmanaged Sistema: syscall, mmap, página de memória, kernel