Gerenciamento de Memória em Zig: Allocators Explicados

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:

ProblemaImpacto
Pause the worldA aplicação congela durante a coleta
Memória imprevisívelUso de memória maior que o necessário
Overhead de runtimeGC consome CPU constantemente
ImprevisibilidadeNã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ê:

  1. Escolha explicitamente qual allocator usar
  2. Passe o allocator como parâmetro para funções que precisam de memória
  3. Libere memória explicitamente (com defer ou 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

AspectoCom 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étodoDescriçãoRetorno
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 existentebool (sucesso)
free(ptr)Libera memória alocadavoid
destroy(ptr)Libera memória de único itemvoid

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 chamador
  • defer: Garante que free será 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áveisPrecisa de máxima performance em hot path
Quer detecção de bugs em debugAlocações em loop apertado
Não sabe o tamanho máximo necessárioMemó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 é conhecidoTamanho é imprevisível
Performance é críticaPrecisa de alocações que persistem além do escopo
Em sistemas embarcadosPrecisa 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 deinit libera 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çãoPrecisa liberar memória em momentos diferentes
Ciclo de vida é bem definidoMemória precisa persistir indefinidamente
Parser, compiladores, servidores HTTPRecursos 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árioAllocator Recomendado
Uso geralGPA
Alocações temporárias de tamanho conhecidoFBA
Parser, compiladorArena
Sistemas embarcadosFBA ou customizado
Hot path, máxima performanceFBA ou Pool
Interop com CCAllocator

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

Recursos Adicionais


Dúvidas sobre allocators? Junte-se à nossa comunidade no Discord!

Continue aprendendo Zig

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