Alocadores Customizados em Zig — Estratégias Avançadas de Memória

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árioAlocador Recomendado
Desenvolvimento geralGeneralPurposeAllocator
Processamento de requestsArenaAllocator
Objetos de tamanho fixoPool Allocator
Sem heap (embarcados)FixedBufferAllocator
Grandes blocos alinhadospage_allocator
Interop com Cc_allocator
Testesstd.testing.allocator

Boas Práticas

  1. Sempre aceite allocator como parâmetro: Nunca use alocadores globais
  2. Use defer para liberação: Garanta que memória é liberada mesmo em caso de erro
  3. ArenaAllocator para alocações temporárias: Elimina overhead de free individual
  4. Teste com FailingAllocator: Garanta que seu código trata OutOfMemory
  5. 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.

Continue aprendendo Zig

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