Stack vs Heap em Zig — O que é e Como Usar
Definição
Stack (pilha) e Heap (monte) são as duas regiões principais de memória onde dados podem ser armazenados durante a execução de um programa Zig.
- Stack: Memória rápida, automática, com tamanho fixo, alocada e liberada em ordem LIFO (último a entrar, primeiro a sair). Variáveis locais vivem na stack.
- Heap: Memória dinâmica, gerenciada manualmente via allocators, com tamanho flexível. Dados que precisam sobreviver além do escopo da função vivem no heap.
Em Zig, diferente de linguagens com garbage collector, você controla explicitamente quando dados vão para o heap.
Por que Entender Stack vs Heap Importa
- Performance: Stack é ordens de magnitude mais rápida que heap (alocação = mover ponteiro vs syscall).
- Tempo de vida: Dados na stack morrem ao sair do escopo; dados no heap vivem até serem liberados.
- Tamanho: Stack é limitada (tipicamente 8MB); heap é limitada pela RAM disponível.
- Previsibilidade: Stack não fragmenta; heap pode fragmentar com muitas alocações/liberações.
Exemplo Prático
Dados na Stack
const std = @import("std");
pub fn main() void {
// Todas estas variáveis vivem na stack
var x: u32 = 42;
var buffer: [256]u8 = undefined;
const ponto = struct { x: f64, y: f64 }{ .x = 1.0, .y = 2.0 };
x += 1;
buffer[0] = 'A';
std.debug.print("x={}, ponto=({d},{d})\n", .{ x, ponto.x, ponto.y });
// Ao sair de main, tudo é liberado automaticamente
}
Dados no Heap
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Alocação no heap — tamanho dinâmico
const tamanho: usize = obterTamanho(); // valor só conhecido em runtime
const buffer = try allocator.alloc(u8, tamanho);
defer allocator.free(buffer);
// Lista que cresce dinamicamente
var lista = std.ArrayList(u32).init(allocator);
defer lista.deinit();
try lista.append(1);
try lista.append(2);
try lista.append(3);
std.debug.print("Itens: {}\n", .{lista.items.len});
}
fn obterTamanho() usize {
return 1024;
}
Retornando Dados: Stack vs Heap
// FUNCIONA: retorna cópia do valor (struct pequena)
fn criarPonto(x: f64, y: f64) struct { x: f64, y: f64 } {
return .{ .x = x, .y = y };
}
// PERIGOSO: retornar ponteiro para variável local!
fn perigoso() *u32 {
var local: u32 = 42;
return &local; // ERRO: dangling pointer!
}
// CORRETO: alocar no heap quando precisa do ponteiro
fn seguro(allocator: std.mem.Allocator) !*u32 {
const ptr = try allocator.create(u32);
ptr.* = 42;
return ptr; // OK: dados vivem no heap
}
Comparação Detalhada
| Característica | Stack | Heap |
|---|---|---|
| Velocidade | Muito rápida | Mais lenta |
| Tamanho | Limitado (~8MB) | Limitado pela RAM |
| Gerenciamento | Automático | Manual (allocator) |
| Fragmentação | Nenhuma | Possível |
| Tamanho dos dados | Fixo em compilação | Dinâmico em runtime |
| Thread safety | Cada thread tem sua stack | Compartilhado entre threads |
| Localidade de cache | Excelente | Variável |
Quando Usar Cada Um
Use Stack quando:
- O tamanho é conhecido em tempo de compilação
- Os dados são pequenos (< alguns KB)
- Os dados só são necessários no escopo atual
Use Heap quando:
- O tamanho é determinado em runtime
- Os dados são grandes (ex: buffers de MB)
- Os dados precisam sobreviver além do escopo
- Você precisa de estruturas que crescem (ArrayList, HashMap)
O Terceiro Caminho: Stack com FixedBufferAllocator
O FixedBufferAllocator combina o melhor dos dois mundos: usa um buffer da stack como backing store, mas expõe a interface de Allocator, permitindo estruturas dinâmicas (ArrayList, HashMap) sem nenhuma syscall ou fragmentação de heap. Quando o buffer da stack é destruído ao sair do escopo, toda a memória é liberada automaticamente — sem nenhum defer free individual necessário.
Perguntas Frequentes
Posso aumentar o tamanho da stack? Sim, mas é dependente de plataforma. Em Zig, ao criar threads com std.Thread.spawn, você pode especificar o tamanho da stack. Para a thread principal, o limite é definido pelo sistema operacional.
Structs grandes devem ir para o heap? Não necessariamente. Uma struct de 4KB na stack é segura. O problema são arrays de MB. Use bom senso: se cabe razoavelmente na stack (< centenas de KB) e tem tempo de vida claro, deixe na stack.
Passar por valor ou por ponteiro? Para tipos pequenos (< 16 bytes), passar por valor é geralmente mais rápido. Para tipos grandes, passe por ponteiro ou referência para evitar cópias.
Armadilhas Comuns
- Stack overflow: Arrays muito grandes na stack (ex:
var buffer: [10_000_000]u8) causam stack overflow. Use heap para dados grandes. - Dangling pointers: Retornar ponteiros para variáveis locais da stack é comportamento indefinido.
- Memory leaks: Esquecer de liberar memória do heap com
defer allocator.free(). - Assumir tamanho da stack: O tamanho da stack varia entre plataformas. Não assuma 8MB.
Termos Relacionados
- Allocator — Interface de alocação de memória
- Arena Allocator — Alocador que libera tudo junto
- Memory Leak — Vazamentos de memória
- Dangling Pointer — Ponteiros pendentes