Pool de Objetos em Zig
O padrão Pool de Objetos mantém uma coleção de objetos pré-alocados e reutilizáveis, evitando o custo de alocação e desalocação frequente. Em Zig, com controle explícito de memória, este padrão é especialmente útil para aplicações de alta performance, servidores e sistemas embarcados.
Quando Usar
- Servidores que criam/destroem conexões frequentemente
- Game loops com criação/destruição de entidades
- Pools de buffers para I/O
- Sistemas embarcados com memória limitada
- Evitar fragmentação de memória
Implementação Básica
const std = @import("std");
fn Pool(comptime T: type, comptime tamanho: usize) type {
return struct {
const Self = @This();
itens: [tamanho]T = undefined,
disponiveis: [tamanho]bool = [_]bool{true} ** tamanho,
inicializador: *const fn () T,
pub fn init(inicializador: *const fn () T) Self {
var self = Self{
.inicializador = inicializador,
};
for (&self.itens) |*item| {
item.* = inicializador();
}
return self;
}
pub fn adquirir(self: *Self) ?*T {
for (self.disponiveis, 0..) |*disp, i| {
if (disp.*) {
disp.* = false;
return &self.itens[i];
}
}
return null; // pool esgotado
}
pub fn liberar(self: *Self, ptr: *T) void {
const inicio = @intFromPtr(&self.itens[0]);
const endereco = @intFromPtr(ptr);
const indice = (endereco - inicio) / @sizeOf(T);
if (indice < tamanho) {
self.itens[indice] = (self.inicializador)(); // resetar
self.disponiveis[indice] = true;
}
}
pub fn disponiveisCount(self: *const Self) usize {
var count: usize = 0;
for (self.disponiveis) |d| {
if (d) count += 1;
}
return count;
}
};
}
const Buffer = struct {
dados: [1024]u8 = undefined,
tamanho: usize = 0,
fn criar() Buffer {
return Buffer{};
}
};
pub fn main() void {
var pool = Pool(Buffer, 16).init(Buffer.criar);
// Adquirir buffer do pool
if (pool.adquirir()) |buf| {
buf.dados[0] = 'A';
buf.tamanho = 1;
std.debug.print("Buffer adquirido, disponíveis: {d}\n", .{pool.disponiveisCount()});
// Devolver ao pool quando terminar
pool.liberar(buf);
std.debug.print("Buffer liberado, disponíveis: {d}\n", .{pool.disponiveisCount()});
}
}
Pool com Allocator Dinâmico
const std = @import("std");
fn PoolDinamico(comptime T: type) type {
return struct {
const Self = @This();
livres: std.ArrayList(*T),
allocator: std.mem.Allocator,
total_criados: usize = 0,
max_tamanho: usize,
pub fn init(allocator: std.mem.Allocator, max: usize) Self {
return .{
.livres = std.ArrayList(*T).init(allocator),
.allocator = allocator,
.max_tamanho = max,
};
}
pub fn deinit(self: *Self) void {
for (self.livres.items) |item| {
self.allocator.destroy(item);
}
self.livres.deinit();
}
pub fn adquirir(self: *Self) !*T {
if (self.livres.popOrNull()) |item| {
return item;
}
// Criar novo se abaixo do limite
if (self.total_criados < self.max_tamanho) {
const novo = try self.allocator.create(T);
novo.* = std.mem.zeroes(T);
self.total_criados += 1;
return novo;
}
return error.PoolEsgotado;
}
pub fn liberar(self: *Self, item: *T) !void {
item.* = std.mem.zeroes(T); // resetar
try self.livres.append(item);
}
};
}
Pool com FixedBufferAllocator para Sistemas Embarcados
Em sistemas com memória estática, você pode criar um pool sem alocação dinâmica alguma:
const std = @import("std");
fn PoolEstatico(comptime T: type, comptime N: usize) type {
return struct {
const Self = @This();
itens: [N]T = undefined,
em_uso: std.StaticBitSet(N) = std.StaticBitSet(N).initEmpty(),
pub fn adquirir(self: *Self) ?*T {
const idx = self.em_uso.findFirstUnset() orelse return null;
self.em_uso.set(idx);
self.itens[idx] = std.mem.zeroes(T);
return &self.itens[idx];
}
pub fn liberar(self: *Self, ptr: *T) void {
const base = @intFromPtr(&self.itens[0]);
const addr = @intFromPtr(ptr);
const idx = (addr - base) / @sizeOf(T);
std.debug.assert(idx < N);
self.em_uso.unset(idx);
}
pub fn emUso(self: *const Self) usize {
return self.em_uso.count();
}
};
}
// Uso: pool completamente estático, sem heap
var pool_conexoes = PoolEstatico(Conexao, 32){};
std.StaticBitSet é muito mais eficiente que o array de bool para verificar disponibilidade — especialmente para pools grandes, onde o bit set cabe em alguns registradores.
Considerações de Performance
- Pool estático vs dinâmico: o pool estático (tamanho fixo em comptime) não faz nenhuma syscall e vive completamente no segmento de dados ou stack. Para servidores com carga previsível, isso elimina toda latência de alocação.
- Resetar o estado ao liberar: chamar
std.mem.zeroes(T)ao adquirir garante que o objeto começa em estado limpo. Se o objeto tem campos que não precisam ser zerados (ex: buffers que serão completamente reescritos), você pode pular o zero para ganhar performance. - Fragmentação não existe em pools: ao contrário de um allocator de propósito geral, o pool não fragmenta — todos os slots têm o mesmo tamanho. Isso torna o comportamento de memória completamente previsível ao longo do tempo.
StaticBitSet.findFirstUnseté O(N/wordsize): para pools de até 64 objetos, encontrar um slot livre leva apenas uma instrução de bit (tzcnt). Para pools maiores, ainda é muito eficiente.
Erros Comuns
Retornar um objeto ao pool errado: se você tem dois pools do mesmo tipo e retorna um ponteiro ao pool errado, o índice calculado será inválido. Adicione uma asserção: std.debug.assert(addr >= base and addr < base + N * @sizeOf(T)).
Usar o objeto após liberá-lo: após pool.liberar(ptr), o ponteiro ptr pode ser adquirido por outro cliente. Qualquer acesso após a liberação é undefined behavior. Considere anular o ponteiro após liberar: defer { pool.liberar(conn); conn = undefined; }.
Pool muito pequeno causando null inesperado: dimensione o pool com folga. Meça o pico real de uso concorrente em produção e adicione 20-30% de margem. Um adquirir que retorna null em produção é tão problemático quanto um OutOfMemory.
Perguntas Frequentes
Quando usar Pool de Objetos vs Arena Allocator? Use Pool de Objetos quando você precisa liberar objetos individualmente em momentos diferentes. Use Arena quando você cria muitos objetos e libera todos ao mesmo tempo ao final de uma operação (ex: processar uma requisição HTTP).
Como tornar o pool thread-safe?
Adicione um std.Thread.Mutex ao pool. Bloqueie para adquirir e liberar. Para alta contenção, considere pools por thread (thread-local storage) com um pool central de reserva — padrão usado em alocadores de alta performance como jemalloc.
O pool garante que objetos são inicializados corretamente?
Apenas se você inicializar no adquirir. O pool é responsável pela alocação, não pela inicialização semântica. Para objetos que precisam de setup complexo (init com alocações), crie-os via init após adquirir o slot do pool.
Quando Evitar
- Objetos pequenos e baratos de criar (overhead do pool supera o benefício)
- Quando o tamanho do pool é difícil de prever
- Objetos com estado complexo difícil de resetar
- Aplicações onde memória não é gargalo
Veja Também
- Arena Pattern — Alocação em bloco sem reutilização individual
- Allocators — Alocadores de memória em Zig
- Flyweight — Compartilhar estado entre objetos
- Concorrência — Pool thread-safe
- FAQ Performance — Otimização de memória