Como Usar Pool Allocators em Zig

Introdução

Um pool allocator (alocador de pool) pré-aloca blocos de memória de tamanho fixo e os reutiliza conforme necessário. Isso elimina a fragmentação e torna a alocação/desalocação extremamente rápida (O(1)). Pools são ideais para cenários onde muitos objetos do mesmo tipo são frequentemente criados e destruídos, como partículas em jogos, nós de grafos ou conexões de rede.

Nesta receita, você aprenderá a implementar e usar pool allocators em Zig.

Pré-requisitos

Implementação de Pool Simples

Pool de objetos de tamanho fixo com lista livre:

const std = @import("std");

fn Pool(comptime T: type, comptime pool_size: usize) type {
    return struct {
        const Self = @This();

        const Node = struct {
            data: T,
            next_free: ?*Node,
        };

        nodes: [pool_size]Node = undefined,
        free_list: ?*Node = null,
        alocados: usize = 0,

        pub fn init() Self {
            var self = Self{};
            // Construir lista livre
            var i: usize = pool_size;
            while (i > 0) {
                i -= 1;
                self.nodes[i].next_free = self.free_list;
                self.free_list = &self.nodes[i];
            }
            return self;
        }

        pub fn create(self: *Self) !*T {
            const node = self.free_list orelse return error.PoolExhausted;
            self.free_list = node.next_free;
            self.alocados += 1;
            node.data = undefined;
            return &node.data;
        }

        pub fn destroy(self: *Self, ptr: *T) void {
            const node: *Node = @fieldParentPtr("data", ptr);
            node.next_free = self.free_list;
            self.free_list = node;
            self.alocados -= 1;
        }

        pub fn disponivel(self: *Self) usize {
            return pool_size - self.alocados;
        }
    };
}

const Particula = struct {
    x: f32,
    y: f32,
    vx: f32,
    vy: f32,
    vida: f32,
};

pub fn main() !void {
    var pool = Pool(Particula, 100).init();

    // Criar partículas
    var particulas: [10]*Particula = undefined;
    for (&particulas, 0..) |*p, i| {
        p.* = try pool.create();
        const fi: f32 = @floatFromInt(i);
        p.*.* = .{
            .x = fi * 10.0,
            .y = fi * 5.0,
            .vx = 1.0,
            .vy = -0.5,
            .vida = 100.0,
        };
    }

    std.debug.print("Partículas criadas: {d}\n", .{pool.alocados});
    std.debug.print("Disponíveis: {d}\n", .{pool.disponivel()});

    // Simular: destruir algumas
    for (particulas[0..5]) |p| {
        pool.destroy(p);
    }
    std.debug.print("Após destruir 5: {d} alocadas, {d} disponíveis\n", .{
        pool.alocados,
        pool.disponivel(),
    });

    // Criar novas (reutiliza a memória)
    const nova = try pool.create();
    nova.* = .{ .x = 0, .y = 0, .vx = 2, .vy = 2, .vida = 200 };
    std.debug.print("Nova partícula criada. Total: {d}\n", .{pool.alocados});
}

Saída esperada

Partículas criadas: 10
Disponíveis: 90
Após destruir 5: 5 alocadas, 95 disponíveis
Nova partícula criada. Total: 6

Pool Dinâmico com Alocador

Pool que pode crescer usando um alocador subjacente:

const std = @import("std");

fn DynamicPool(comptime T: type) type {
    return struct {
        const Self = @This();

        items: std.ArrayList(T),
        free_indices: std.ArrayList(usize),

        pub fn init(allocator: std.mem.Allocator) Self {
            return .{
                .items = std.ArrayList(T).init(allocator),
                .free_indices = std.ArrayList(usize).init(allocator),
            };
        }

        pub fn deinit(self: *Self) void {
            self.items.deinit();
            self.free_indices.deinit();
        }

        pub fn acquire(self: *Self) !usize {
            if (self.free_indices.popOrNull()) |idx| {
                return idx;
            }
            // Sem índice livre, expandir
            try self.items.append(undefined);
            return self.items.items.len - 1;
        }

        pub fn release(self: *Self, idx: usize) !void {
            try self.free_indices.append(idx);
        }

        pub fn get(self: *Self, idx: usize) *T {
            return &self.items.items[idx];
        }

        pub fn ativas(self: *Self) usize {
            return self.items.items.len - self.free_indices.items.len;
        }
    };
}

const Conexao = struct {
    id: u32,
    ativa: bool,
    endereco: []const u8,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var pool = DynamicPool(Conexao).init(allocator);
    defer pool.deinit();

    // Simular conexões
    const idx1 = try pool.acquire();
    pool.get(idx1).* = .{ .id = 1, .ativa = true, .endereco = "192.168.1.1" };

    const idx2 = try pool.acquire();
    pool.get(idx2).* = .{ .id = 2, .ativa = true, .endereco = "192.168.1.2" };

    const idx3 = try pool.acquire();
    pool.get(idx3).* = .{ .id = 3, .ativa = true, .endereco = "192.168.1.3" };

    std.debug.print("Conexões ativas: {d}\n", .{pool.ativas()});

    // Liberar uma conexão
    try pool.release(idx2);
    std.debug.print("Após liberar idx2: {d} ativas\n", .{pool.ativas()});

    // Reutilizar slot
    const idx4 = try pool.acquire(); // Reutiliza idx2
    pool.get(idx4).* = .{ .id = 4, .ativa = true, .endereco = "192.168.1.4" };
    std.debug.print("Nova conexão no slot {d}: {s}\n", .{ idx4, pool.get(idx4).endereco });
}

Quando Usar Pool Allocator

CenárioPool?Por quê?
Partículas em jogosSimCriação/destruição frequente, tamanho fixo
Nós de listas/grafosSimMuitos objetos iguais
Conexões de redeSimPool de conexões é padrão
Strings de tamanho variávelNãoUse ArenaAllocator
Dados de longa duraçãoNãoUse GPA

Dicas e Boas Práticas

  1. Pool fixo para performance máxima: Sem alocação dinâmica, tempo constante garantido.

  2. Pool dinâmico para flexibilidade: Quando o número máximo não é conhecido antecipadamente.

  3. Reutilize sempre: Pools brilham quando objetos são criados e destruídos frequentemente.

  4. Inicialize ao adquirir: Sempre defina valores ao adquirir um slot do pool, pois pode conter dados antigos.

  5. Combine com ArenaAllocator: Para objetos com dados de tamanho variável, use arena para os dados e pool para os objetos.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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