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ção | Descriçã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?
| Allocator | Melhor Para | Evitar Quando | Performance |
|---|---|---|---|
page_allocator | Backing de outros allocators | Alocações pequenas | Baixa |
GPA | Desenvolvimento e debug | Hot paths extremos | Média |
FixedBufferAllocator | Embarcados, sem heap | Tamanho imprevisível | Alta |
ArenaAllocator | Alocações temporárias em lote | Liberação individual | Muito 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
- Documentação std.mem.Allocator — Referência completa da interface
- Stack vs Heap (Artigo 1) — Artigo anterior da série
- Receitas de Memória — Exemplos prontos para uso