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:
// 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.
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.
O catálogo da std
Section titled “O catálogo da std”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:
| Allocator | De onde vêm os bytes | Como libera | Ideal para |
|---|---|---|---|
page_allocator | direto do SO, uma página por vez | por alocação | poucas alocações grandes; base dos outros |
DebugAllocator (ex-GeneralPurposeAllocator) | de um pool próprio, vigiado | por alocação; acusa vazamento no deinit | desenvolvimento e testes |
ArenaAllocator | de um bloco que cresce | tudo de uma vez, num deinit | tempo de vida por fases |
FixedBufferAllocator | de um buffer que você já tem | tudo de uma vez, sem syscall | embarcado, 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.
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.
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);defer arena.deinit(); // libera tudo que a arena distribuiu, de uma vezconst 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.
Por que isso destrava o resto
Section titled “Por que isso destrava o resto”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.
var lista: std.ArrayListUnmanaged(u32) = .{};defer lista.deinit(allocator); // o allocator volta aquitry lista.append(allocator, 42); // e aqui, a cada crescimentotry 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.
Palavras-chave
Section titled “Palavras-chave”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