Uma das características mais distintivas de Zig é seu modelo de gerenciamento de memória. Enquanto a maioria das linguagens modernas escolhe entre garbage collector (Go, Java, C#) ou borrow checker (Rust), Zig adota uma abordagem diferente: allocators explícitos.
Este tutorial é um guia completo sobre como Zig gerencia memória. Se você vem de linguagens com GC, isso pode parecer assustador no início. Mas dominar allocators é essencial para escrever código Zig eficiente e seguro.
Pré-requisitos: Conhecimento básico de Zig (variáveis, funções, slices). Se você é novo no Zig, comece com nosso guia de instalação.
1. Introdução: Por que Zig Não Tem Garbage Collector?
O Problema com Garbage Collectors
Garbage collectors (GCs) automatizam o gerenciamento de memória, mas têm custos:
| Problema | Impacto |
|---|---|
| Pause the world | A aplicação congela durante a coleta |
| Memória imprevisível | Uso de memória maior que o necessário |
| Overhead de runtime | GC consome CPU constantemente |
| Imprevisibilidade | Não controle quando memória é liberada |
Para sistemas embarcados, jogos, engines de banco de dados e serviços de alta performance, esses custos são inaceitáveis.
A Filosofia Zig: Explicit Over Implicit
Zig segue o princípio de que comportamento implícito esconde bugs. Em vez de um GC mágico escondido, Zig exige que você:
- Escolha explicitamente qual allocator usar
- Passe o allocator como parâmetro para funções que precisam de memória
- Libere memória explicitamente (com
deferou manualmente)
Isso parece trabalhoso, mas traz benefícios enormes:
- Controle total sobre quando e onde memória é alocada
- Performance previsível — sem pauses inesperadas
- Uso eficiente de memória — aloque exatamente o que precisa
- Código mais claro — comportamento de memória é visível no código
Comparativo de Abordagens
| Aspecto | Com GC (Go/Java) | Com Borrow Checker (Rust) | Com Allocators (Zig) |
|---|---|---|---|
| Segurança de memória | ✅ Runtime (GC) | ✅ Compilação | ⚠️ Runtime (debug builds) |
| Performance | ⚠️ Pauses | ✅ Zero-cost | ✅ Zero-cost |
| Controle | ❌ Pouco | ⚠️ Moderado | ✅ Total |
| Curva de aprendizado | ✅ Fácil | ❌ Íngreme | ⚠️ Moderada |
| Uso de memória | ❌ Overhead | ✅ Eficiente | ✅ Eficiente |
2. O Modelo de Allocators
Em Zig, um allocator é qualquer tipo que implementa a interface Allocator. Vamos entender como usar allocators na prática.
A Interface Allocator
const std = @import("std");
pub fn main() !void {
// 1. Inicializa um allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// 2. Obtém a interface Allocator
const allocator = gpa.allocator();
// 3. Usa para alocar memória
const memoria = try allocator.alloc(u8, 100);
defer allocator.free(memoria);
// 4. Usa a memória
@memcpy(memoria, "Olá, Zig!");
std.debug.print("{s}\n", .{memoria});
}
Métodos Principais da Interface Allocator
| Método | Descrição | Retorno |
|---|---|---|
alloc(T, n) | Aloca array de n elementos do tipo T | []T ou error |
create(T) | Aloca espaço para um único T | *T ou error |
resize(old, new_size) | Redimensiona alocação existente | bool (sucesso) |
free(ptr) | Libera memória alocada | void |
destroy(ptr) | Libera memória de único item | void |
O Padrão try + defer
O padrão idiomático em Zig é:
const memoria = try allocator.alloc(u8, tamanho);
defer allocator.free(memoria);
try: Propaga erros (como OutOfMemory) para o chamadordefer: Garante quefreeserá chamado quando sair do escopo
Isso previne memory leaks — mesmo se a função retornar cedo por erro, a memória é liberada.
3. GeneralPurposeAllocator (GPA)
O GeneralPurposeAllocator é o “canivete suíço” dos allocators Zig. É seguro, detecta bugs, e é adequado para a maioria dos casos.
Características do GPA
- ✅ Segurança em debug: Detecta double-free, use-after-free, memory leaks
- ✅ Thread-safe: Pode ser usado de múltiplas threads
- ⚠️ Overhead: Verificações adicionais em debug builds
- ✅ Liberação individual: Cada alocação pode ser liberada separadamente
Uso Básico
const std = @import("std");
pub fn main() !void {
// Inicializa o GPA
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// Importante: deinit retorna informação de leaks!
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
std.debug.print("⚠️ Memory leak detectado!\n", .{});
}
}
const allocator = gpa.allocator();
// Aloca um array
const nomes = try allocator.alloc([]const u8, 3);
defer allocator.free(nomes);
nomes[0] = "Alice";
nomes[1] = "Bob";
nomes[2] = "Carol";
for (nomes) |nome| {
std.debug.print("{s}\n", .{nome});
}
}
GPA em Release Builds
Em builds de release (-O ReleaseFast ou -O ReleaseSmall), o GPA desabilita verificações de segurança, tornando-se mais rápido:
// Debug build: verificações ativadas
zig build-exe programa.zig
// Release build: otimizado, sem verificações
zig build-exe -O ReleaseFast programa.zig
Quando Usar GPA
| Use GPA quando… | Não use quando… |
|---|---|
| Precisa de alocações dinâmicas variáveis | Precisa de máxima performance em hot path |
| Quer detecção de bugs em debug | Alocações em loop apertado |
| Não sabe o tamanho máximo necessário | Memória limitada (embarcados) |
4. FixedBufferAllocator (FBA)
O FixedBufferAllocator aloca memória de um buffer fixo — geralmente na stack ou em memória estática. Não faz chamadas ao sistema operacional!
Características do FBA
- ✅ Zero overhead: Sem chamadas ao SO
- ✅ Previsível: Memória limitada a um buffer conhecido
- ✅ Determinístico: Sem alocações dinâmicas imprevisíveis
- ⚠️ Limitado: Quando o buffer acaba, alocações falham
Uso Básico na Stack
const std = @import("std");
pub fn processarDados(dados: []const u8) !void {
// Buffer na stack (8KB)
var buffer: [8192]u8 = undefined;
// FBA usa esse buffer
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
// Alocações vêm do buffer na stack
const copia = try allocator.dupe(u8, dados);
// Não precisa de defer free! Memória é liberada quando função retorna
// Processa...
std.debug.print("Processando: {s}\n", .{copia});
// Buffer é automaticamente liberado quando função retorna
}
pub fn main() !void {
try processarDados("Dados importantes");
}
Uso com Memória Estática
const std = @import("std");
// Buffer global (útil para singletons)
var buffer_global: [1024 * 1024]u8 = undefined; // 1MB
var fba = std.heap.FixedBufferAllocator.init(&buffer_global);
pub fn alocacaoEstatica() !void {
const allocator = fba.allocator();
const dados = try allocator.alloc(u8, 1000);
// use dados...
// Para reutilizar o buffer, reset:
fba.reset();
}
Resetando o FBA
// Reset libera TODA a memória de uma vez
fba.reset();
// Ou reset mantendo um ponto de rollback
const checkpoint = fba.end_index;
// ... faz alocações ...
fba.end_index = checkpoint; // "Desfaz" alocações desde checkpoint
Quando Usar FBA
| Use FBA quando… | Não use quando… |
|---|---|
| Tamanho máximo é conhecido | Tamanho é imprevisível |
| Performance é crítica | Precisa de alocações que persistem além do escopo |
| Em sistemas embarcados | Precisa de thread-safety (FBA não é thread-safe) |
| Em funções que alocam temporariamente |
5. Arena Allocator
O ArenaAllocator é um dos padrões mais úteis em Zig. Você aloca muitas vezes, mas libera tudo de uma vez.
Características da Arena
- ✅ Liberação em bulk: Um
deinitlibera tudo - ✅ Rápido: Alocação é apenas incrementar um ponteiro
- ✅ Simples: Não precisa rastrear alocações individuais
- ⚠️ Sem liberação individual: Não pode liberar itens específicos
Uso Básico
const std = @import("std");
pub fn processarRequisicao(allocator: std.mem.Allocator) !void {
// Arena que usa GPA como backend
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); // Libera TUDO aqui
const arena_allocator = arena.allocator();
// Faça quantas alocações quiser...
const headers = try arena_allocator.alloc(u8, 100);
const body = try arena_allocator.alloc(u8, 1000);
const response = try arena_allocator.alloc(u8, 500);
// Não precisa de defer free para cada um!
// Simula processamento...
@memcpy(headers, "HTTP/1.1 200 OK");
@memcpy(body, "Conteúdo da resposta...");
@memcpy(response, "Resposta completa");
std.debug.print("{s}\n", .{headers});
// Tudo é liberado automaticamente no defer arena.deinit()
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
try processarRequisicao(gpa.allocator());
}
Padrão Comum: Funções que Alocam
// Função recebe allocator como parâmetro
pub fn parseJSON(allocator: std.mem.Allocator, json: []const u8) !Value {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
// Parse usa arena para todas as alocações temporárias
const result = try parseInternal(arena.allocator(), json);
// Duplica resultado para fora da arena (se necessário)
return result.clone(allocator);
}
Quando Usar Arena
| Use Arena quando… | Não use quando… |
|---|---|
| Muitas alocações de curta duração | Precisa liberar memória em momentos diferentes |
| Ciclo de vida é bem definido | Memória precisa persistir indefinidamente |
| Parser, compiladores, servidores HTTP | Recursos escassos que precisam ser liberados ASAP |
6. PageAllocator
O PageAllocator é o mais fundamental: pede páginas de memória diretamente ao sistema operacional.
Características
- ✅ Simples: Sem overhead de bookkeeping
- ✅ Alinhado: Retorna memória alinhada a página
- ⚠️ Lento: Syscall para cada alocação
- ⚠️ Granularidade grossa: Aloca páginas inteiras (4KB+)
Uso Direto
const std = @import("std");
pub fn main() !void {
// Page allocator direto
const allocator = std.heap.page_allocator;
// Aloca 1 byte → recebe página inteira (4KB)
const um_byte = try allocator.alloc(u8, 1);
defer allocator.free(um_byte);
std.debug.print("Alocado: {} bytes\n", .{um_byte.len});
}
Aviso: PageAllocator é ineficiente para pequenas alocações. Use GPA ou FBA para isso.
Como Backend
PageAllocator é mais usado como backend para outros allocators:
// GPA usa page_allocator internamente
var gpa = std.heap.GeneralPurposeAllocator(.{
.backing_allocator = std.heap.page_allocator,
}){};
7. CAllocator
O CAllocator usa malloc/free da libc — útil para interoperabilidade com código C.
Uso
const std = @import("std");
pub fn main() !void {
const c_allocator = std.heap.c_allocator;
const memoria = try c_allocator.alloc(u8, 100);
defer c_allocator.free(memoria);
// Pode passar para funções C que esperam malloc'd memory
}
Quando Usar CAllocator
- Integrando com bibliotecas C que esperam
malloc/free - Substituindo código C gradualmente
- Quando libc já está linkada e você quer consistência
8. Criando um Allocator Customizado
Você pode implementar allocators customizados para casos especiais.
Exemplo: Bump Allocator Simples
const std = @import("std");
pub const BumpAllocator = struct {
buffer: []u8,
used: usize,
pub fn init(buffer: []u8) BumpAllocator {
return .{
.buffer = buffer,
.used = 0,
};
}
pub fn allocator(self: *BumpAllocator) std.mem.Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.free = free,
},
};
}
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
const self = @as(*BumpAllocator, @ptrCast(@alignCast(ctx)));
// Alinha o endereço
const addr = @intFromPtr(self.buffer.ptr) + self.used;
const aligned_addr = std.mem.alignForward(addr, ptr_align);
const offset = aligned_addr - @intFromPtr(self.buffer.ptr);
// Verifica se cabe
if (offset + len > self.buffer.len) return null;
self.used = offset + len;
return self.buffer.ptr + offset;
}
fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
_ = ctx; _ = buf; _ = buf_align; _ = new_len; _ = ret_addr;
// Bump allocator não suporta resize
return false;
}
fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
_ = ctx; _ = buf; _ = buf_align; _ = ret_addr;
// Bump allocator não libera individualmente
}
pub fn reset(self: *BumpAllocator) void {
self.used = 0;
}
};
pub fn main() !void {
var buffer: [1024]u8 = undefined;
var bump = BumpAllocator.init(&buffer);
const allocator = bump.allocator();
const dados = try allocator.alloc(u8, 100);
std.debug.print("Alocado: {} bytes em {:p}\n", .{dados.len, dados.ptr});
}
9. Error Handling com Allocators
Alocações podem falhar (OutOfMemory). Em Zig, isso é tratado explicitamente.
O Erro OutOfMemory
const memoria = allocator.alloc(u8, tamanho_enorme);
// Tratamento com if/else
if (memoria) |mem| {
// Sucesso, use mem
defer allocator.free(mem);
} else |err| {
std.debug.print("Falha ao alocar: {}\n", .{err});
return err;
}
Padrão try/catch
pub fn alocacaoSegura(allocator: std.mem.Allocator) !void {
// Propaga erro para o chamador
const memoria = try allocator.alloc(u8, 1000);
defer allocator.free(memoria);
// Ou trata localmente
const mais_memoria = allocator.alloc(u8, 1_000_000) catch |err| {
std.log.err("Não foi possível alocar 1MB: {}", .{err});
return error.AlocacaoGrandeFalhou;
};
defer allocator.free(mais_memoria);
}
Graceful Degradation
pub fn processarComFallback(allocator: std.mem.Allocator, dados: []const u8) !void {
// Tenta alocar otimizado
const buffer = allocator.alloc(u8, dados.len * 2) catch {
// Fallback: processa em chunks menores
return processarEmChunks(allocator, dados);
};
defer allocator.free(buffer);
// Processamento otimizado...
}
10. Melhores Práticas e Padrões Comuns
1. Sempre Use defer para Liberar
// ✅ Bom
const mem = try allocator.alloc(u8, 100);
defer allocator.free(mem);
// ❌ Ruim - esqueceu de liberar
const mem = try allocator.alloc(u8, 100);
// ... código ...
// ops, memory leak!
2. Passe Allocator como Parâmetro
// ✅ Bom - caller escolhe o allocator
pub fn parse(allocator: std.mem.Allocator, input: []const u8) !Result {
const temp = try allocator.alloc(u8, input.len);
defer allocator.free(temp);
// ...
}
// ❌ Ruim - esconde dependência de allocator
pub fn parse(input: []const u8) !Result {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// caller não sabe que você está alocando!
}
3. Use Arena para Alocações Temporárias
// ✅ Arena simplifica gerenciamento
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const a = try arena.allocator().alloc(u8, 100);
const b = try arena.allocator().alloc(u8, 200);
const c = try arena.allocator().alloc(u8, 300);
// Um único defer libera tudo!
4. Escolha o Allocator Certo para o Caso
| Cenário | Allocator Recomendado |
|---|---|
| Uso geral | GPA |
| Alocações temporárias de tamanho conhecido | FBA |
| Parser, compilador | Arena |
| Sistemas embarcados | FBA ou customizado |
| Hot path, máxima performance | FBA ou Pool |
| Interop com C | CAllocator |
5. Detecte Memory Leaks em Debug
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const status = gpa.deinit();
if (status == .leak) {
@panic("Memory leak detectado!");
}
}
Conclusão
O modelo de allocators do Zig pode parecer trabalhoso inicialmente, mas oferece:
- Controle total sobre gerenciamento de memória
- Performance previsível sem pauses de GC
- Flexibilidade para escolher a estratégia certa para cada caso
- Detecção de bugs em debug builds
Dominar allocators é essencial para escrever código Zig idiomático e eficiente. Comece com GPA para casos gerais, experimente FBA para performance crítica, e use Arena quando fizer sentido.
Próximos Passos
- 📚 Leia sobre comptime em Zig para metaprogramação
- 🔧 Explore nosso guia de build system
- 🌐 Experimente compilar para WebAssembly
Recursos Adicionais
Dúvidas sobre allocators? Junte-se à nossa comunidade no Discord!