Alocadores Customizados em Zig — Estratégias Avançadas de Memória
O sistema de alocadores do Zig é um dos seus diferenciais mais importantes. Em vez de ter um alocador global oculto (como malloc em C ou o GC em linguagens gerenciadas), o Zig exige que todo código que aloca memória receba um alocador explicitamente como parâmetro. Isso traz transparência, testabilidade e a capacidade de escolher a estratégia de alocação ideal para cada caso de uso.
A Interface do Allocator
Todo alocador em Zig implementa a mesma interface std.mem.Allocator:
const std = @import("std");
// Qualquer função que aloca memória recebe o alocador
fn processarDados(allocator: std.mem.Allocator, dados: []const u8) ![]u8 {
var resultado = try allocator.alloc(u8, dados.len * 2);
// ... processar ...
return resultado;
}
// O chamador decide qual alocador usar
pub fn main() !void {
// Opção 1: General Purpose Allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const resultado = try processarDados(gpa.allocator(), "dados");
defer gpa.allocator().free(resultado);
}
Alocadores da Biblioteca Padrão
GeneralPurposeAllocator (GPA)
O alocador de uso geral, recomendado para a maioria dos casos:
var gpa = std.heap.GeneralPurposeAllocator(.{
.safety = true, // Detecta erros (double free, use after free)
.thread_safety = .safe, // Thread-safe
.never_unmap = false, // Retorna memória ao OS
.stack_trace_frames = 10, // Stack traces para debug
}){};
defer {
const status = gpa.deinit();
if (status == .leak) {
std.debug.print("Memory leak detectado!\n", .{});
}
}
const allocator = gpa.allocator();
var lista = std.ArrayList(u8).init(allocator);
defer lista.deinit();
ArenaAllocator
Ideal para alocações temporárias que podem ser liberadas em bloco:
pub fn processarRequisicao(request: Request) !Response {
// Todas as alocações são liberadas de uma vez ao final
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit(); // Libera TUDO de uma vez
const allocator = arena.allocator();
// Alocações múltiplas sem preocupação individual
const path = try allocator.dupe(u8, request.path);
const headers = try parseHeaders(allocator, request.raw_headers);
const body = try processarBody(allocator, request.body);
// Construir resposta (sem necessidade de free individual)
return Response{
.status = 200,
.body = try gerarResposta(allocator, path, headers, body),
};
// arena.deinit() libera tudo automaticamente
}
FixedBufferAllocator
Aloca a partir de um buffer fixo, sem recorrer ao OS:
// Perfeito para embarcados e situações sem heap
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const allocator = fba.allocator();
// Alocações limitadas ao tamanho do buffer
var dados = try allocator.alloc(u8, 100);
allocator.free(dados);
page_allocator
Aloca diretamente do sistema operacional em páginas:
const allocator = std.heap.page_allocator;
// Cada alocação é pelo menos uma página (4KB no Linux)
var dados = try allocator.alloc(u8, 8192);
defer allocator.free(dados);
c_allocator
Wrapper sobre malloc/free do C, útil para interoperabilidade:
const allocator = std.heap.c_allocator;
// Usa malloc/free internamente
var dados = try allocator.alloc(u8, 1024);
defer allocator.free(dados);
Alocadores Customizados
Pool Allocator
Para objetos de tamanho fixo com alocação/liberação frequente:
fn PoolAllocator(comptime T: type) type {
return struct {
const Self = @This();
const Node = struct {
data: T,
next: ?*Node,
};
free_list: ?*Node = null,
backing_allocator: std.mem.Allocator,
pub fn init(backing: std.mem.Allocator) Self {
return .{ .backing_allocator = backing };
}
pub fn create(self: *Self) !*T {
if (self.free_list) |node| {
self.free_list = node.next;
return &node.data;
}
const node = try self.backing_allocator.create(Node);
return &node.data;
}
pub fn destroy(self: *Self, ptr: *T) void {
const node: *Node = @fieldParentPtr("data", ptr);
node.next = self.free_list;
self.free_list = node;
}
};
}
// Uso
const Particula = struct {
x: f32, y: f32, z: f32,
vx: f32, vy: f32, vz: f32,
};
var pool = PoolAllocator(Particula).init(std.heap.page_allocator);
var p1 = try pool.create();
p1.* = .{ .x = 0, .y = 0, .z = 0, .vx = 1, .vy = 0, .vz = 0 };
pool.destroy(p1); // Retorna ao pool, não ao OS
var p2 = try pool.create(); // Reutiliza memória do pool
_ = p2;
Stack Allocator (Bump Allocator)
O mais rápido possível para alocações sequenciais:
const StackAllocator = struct {
buffer: []u8,
offset: usize = 0,
pub fn init(buffer: []u8) StackAllocator {
return .{ .buffer = buffer };
}
pub fn alloc(self: *StackAllocator, comptime T: type, n: usize) ![]T {
const bytes_needed = n * @sizeOf(T);
const alignment = @alignOf(T);
const aligned_offset = std.mem.alignForward(usize, self.offset, alignment);
if (aligned_offset + bytes_needed > self.buffer.len) {
return error.OutOfMemory;
}
const ptr = @as([*]T, @ptrCast(@alignCast(self.buffer[aligned_offset..])));
self.offset = aligned_offset + bytes_needed;
return ptr[0..n];
}
pub fn reset(self: *StackAllocator) void {
self.offset = 0;
}
};
Logging Allocator
Para debug e profiling de alocações:
fn LoggingAllocator(comptime InnerAllocator: type) type {
return struct {
inner: InnerAllocator,
total_allocated: usize = 0,
total_freed: usize = 0,
allocation_count: usize = 0,
pub fn allocator(self: *@This()) 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: *@This() = @ptrCast(@alignCast(ctx));
self.total_allocated += len;
self.allocation_count += 1;
std.debug.print("[ALLOC] {} bytes (total: {}, count: {})\n", .{
len, self.total_allocated, self.allocation_count,
});
return self.inner.rawAlloc(len, ptr_align, ret_addr);
}
fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
const self: *@This() = @ptrCast(@alignCast(ctx));
self.total_freed += buf.len;
std.debug.print("[FREE] {} bytes (total freed: {})\n", .{
buf.len, self.total_freed,
});
self.inner.rawFree(buf, buf_align, ret_addr);
}
};
}
Alocadores para Teste
O std.testing.allocator é fundamental para testes:
test "função sem memory leak" {
const allocator = std.testing.allocator;
var lista = std.ArrayList(u8).init(allocator);
defer lista.deinit(); // Leak se esquecer!
try lista.appendSlice("dados de teste");
try std.testing.expect(lista.items.len == 14);
}
test "resiliência a falha de alocação" {
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = 5 },
);
// Testar que a função trata OutOfMemory graciosamente
const result = minhaFuncao(failing.allocator());
try std.testing.expectError(error.OutOfMemory, result);
}
Escolhendo o Alocador Certo
| Cenário | Alocador Recomendado |
|---|---|
| Desenvolvimento geral | GeneralPurposeAllocator |
| Processamento de requests | ArenaAllocator |
| Objetos de tamanho fixo | Pool Allocator |
| Sem heap (embarcados) | FixedBufferAllocator |
| Grandes blocos alinhados | page_allocator |
| Interop com C | c_allocator |
| Testes | std.testing.allocator |
Boas Práticas
- Sempre aceite allocator como parâmetro: Nunca use alocadores globais
- Use
deferpara liberação: Garanta que memória é liberada mesmo em caso de erro - ArenaAllocator para alocações temporárias: Elimina overhead de free individual
- Teste com FailingAllocator: Garanta que seu código trata OutOfMemory
- Profile alocações: Use LoggingAllocator para identificar hotspots
Próximos Passos
Explore os frameworks de teste para testar alocações, as ferramentas de debug para detectar problemas de memória, e as ferramentas de profiling para otimizar uso de memória. Consulte nossos tutoriais e receitas para exemplos práticos.