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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento básico de Zig. Consulte a introdução ao Zig
- Familiaridade com alocadores
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ário | Pool? | Por quê? |
|---|---|---|
| Partículas em jogos | Sim | Criação/destruição frequente, tamanho fixo |
| Nós de listas/grafos | Sim | Muitos objetos iguais |
| Conexões de rede | Sim | Pool de conexões é padrão |
| Strings de tamanho variável | Não | Use ArenaAllocator |
| Dados de longa duração | Não | Use GPA |
Dicas e Boas Práticas
Pool fixo para performance máxima: Sem alocação dinâmica, tempo constante garantido.
Pool dinâmico para flexibilidade: Quando o número máximo não é conhecido antecipadamente.
Reutilize sempre: Pools brilham quando objetos são criados e destruídos frequentemente.
Inicialize ao adquirir: Sempre defina valores ao adquirir um slot do pool, pois pode conter dados antigos.
Combine com ArenaAllocator: Para objetos com dados de tamanho variável, use arena para os dados e pool para os objetos.
Receitas Relacionadas
- Usando ArenaAllocator - Alocação em lote
- Usando GeneralPurposeAllocator - Alocador geral
- Usando FixedBufferAllocator - Buffer fixo
- Operações com Lista Ligada - Nós com pool