O Arena Allocator é provavelmente o padrão de alocação mais poderoso em Zig. No artigo anterior apresentamos brevemente os allocators. Agora vamos explorar o Arena Allocator em profundidade, com exemplos práticos que você pode aplicar imediatamente nos seus projetos.
O Que é um Arena Allocator?
Um Arena Allocator agrupa múltiplas alocações em blocos grandes e as libera todas de uma vez. Em vez de rastrear cada alocação individual, você simplesmente descarta a arena inteira quando terminar.
Pense nisso como um quadro branco: você escreve o quanto quiser, e quando termina, apaga tudo de uma vez — sem precisar apagar cada palavra individualmente.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// Criar arena com backing allocator
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit(); // Uma única chamada libera TUDO
const alloc = arena.allocator();
// Muitas alocações — nenhum defer individual necessário
const nome = try alloc.alloc(u8, 100);
const sobrenome = try alloc.alloc(u8, 100);
const numeros = try alloc.alloc(i32, 50);
const buffer = try alloc.alloc(u8, 4096);
@memcpy(nome[0..3], "Zig");
@memcpy(sobrenome[0..6], "Brasil");
numeros[0] = 42;
buffer[0] = 0xFF;
std.debug.print("Tudo alocado sem nenhum defer individual!\n", .{});
std.debug.print("Nome: {s}, Sobrenome: {s}\n", .{ nome[0..3], sobrenome[0..6] });
// arena.deinit() no defer libera nome, sobrenome, numeros e buffer
}
Caso de Uso 1: Processamento de Requisições HTTP
O exemplo clássico de arena allocator é o processamento de requisições HTTP. Cada requisição aloca memória temporária, e tudo é liberado quando a requisição termina.
const std = @import("std");
const HttpRequest = struct {
method: []const u8,
path: []const u8,
headers: std.StringHashMap([]const u8),
body: []const u8,
};
const HttpResponse = struct {
status: u16,
body: []const u8,
};
fn parseRequest(allocator: std.mem.Allocator, raw: []const u8) !HttpRequest {
var headers = std.StringHashMap([]const u8).init(allocator);
// Simular parsing — todas as alocações usam o allocator da arena
const method = try allocator.alloc(u8, 3);
@memcpy(method, "GET");
const path = try std.fmt.allocPrint(allocator, "/api/users", .{});
try headers.put(
try std.fmt.allocPrint(allocator, "Content-Type", .{}),
try std.fmt.allocPrint(allocator, "application/json", .{}),
);
_ = raw;
return HttpRequest{
.method = method,
.path = path,
.headers = headers,
.body = "",
};
}
fn handleRequest(allocator: std.mem.Allocator, request: HttpRequest) !HttpResponse {
const body = try std.fmt.allocPrint(
allocator,
"{{\"message\": \"Olá do {s} {s}\"}}",
.{ request.method, request.path },
);
return HttpResponse{
.status = 200,
.body = body,
};
}
fn processarRequisicao(backing_allocator: std.mem.Allocator, raw_request: []const u8) !void {
// Arena para esta requisição — tudo é liberado ao final
var arena = std.heap.ArenaAllocator.init(backing_allocator);
defer arena.deinit();
const alloc = arena.allocator();
const request = try parseRequest(alloc, raw_request);
const response = try handleRequest(alloc, request);
std.debug.print("Status: {d}\n", .{response.status});
std.debug.print("Body: {s}\n", .{response.body});
// Ao sair desta função, arena.deinit() libera TODA a memória
// de parsing, headers, body de resposta — tudo de uma vez
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// Simular várias requisições
for (0..3) |i| {
std.debug.print("\n--- Requisição {d} ---\n", .{i + 1});
try processarRequisicao(gpa.allocator(), "GET /api/users HTTP/1.1\r\n");
}
}
Cada requisição cria sua própria arena e, ao terminar, toda a memória é liberada com uma única operação. Isso é mais rápido do que liberar cada alocação individualmente e elimina a possibilidade de memory leaks por alocações esquecidas.
Caso de Uso 2: Parser de Configuração
Parsers são outro caso ideal para arenas, pois geram muitas strings e estruturas intermediárias:
const std = @import("std");
const ConfigValue = union(enum) {
string: []const u8,
integer: i64,
boolean: bool,
};
const Config = struct {
valores: std.StringHashMap(ConfigValue),
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator) Config {
return .{
.valores = std.StringHashMap(ConfigValue).init(allocator),
.allocator = allocator,
};
}
fn set(self: *Config, chave: []const u8, valor: ConfigValue) !void {
const chave_copia = try self.allocator.alloc(u8, chave.len);
@memcpy(chave_copia, chave);
try self.valores.put(chave_copia, valor);
}
fn getString(self: *const Config, chave: []const u8) ?[]const u8 {
if (self.valores.get(chave)) |val| {
switch (val) {
.string => |s| return s,
else => return null,
}
}
return null;
}
fn getInt(self: *const Config, chave: []const u8) ?i64 {
if (self.valores.get(chave)) |val| {
switch (val) {
.integer => |i| return i,
else => return null,
}
}
return null;
}
};
fn parseConfig(allocator: std.mem.Allocator, fonte: []const u8) !Config {
var config = Config.init(allocator);
// Simular parsing de um arquivo de configuração
_ = fonte;
const host = try std.fmt.allocPrint(allocator, "localhost", .{});
try config.set("host", .{ .string = host });
try config.set("port", .{ .integer = 8080 });
try config.set("debug", .{ .boolean = true });
const db_url = try std.fmt.allocPrint(allocator, "postgres://localhost:5432/app", .{});
try config.set("database_url", .{ .string = db_url });
return config;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
// Arena para toda a configuração
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
var config = try parseConfig(arena.allocator(), "host=localhost\nport=8080\n");
if (config.getString("host")) |host| {
std.debug.print("Host: {s}\n", .{host});
}
if (config.getInt("port")) |port| {
std.debug.print("Port: {d}\n", .{port});
}
if (config.getString("database_url")) |url| {
std.debug.print("DB: {s}\n", .{url});
}
}
Caso de Uso 3: Processamento de Dados em Lote
Quando você processa dados em lotes, pode criar uma arena por lote e resetá-la entre iterações:
const std = @import("std");
const Estatisticas = struct {
media: f64,
max: f64,
min: f64,
total: f64,
};
fn processarLote(
allocator: std.mem.Allocator,
dados_brutos: []const f64,
) !Estatisticas {
// Copiar e processar dados usando arena
const dados = try allocator.alloc(f64, dados_brutos.len);
@memcpy(dados, dados_brutos);
// Ordenar (precisaria de buffer auxiliar na arena também)
std.mem.sort(f64, dados, {}, std.sort.asc(f64));
var total: f64 = 0;
for (dados) |v| {
total += v;
}
return Estatisticas{
.media = total / @as(f64, @floatFromInt(dados.len)),
.max = dados[dados.len - 1],
.min = dados[0],
.total = total,
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer arena.deinit();
// Simular vários lotes de dados
const lotes = [_][]const f64{
&.{ 3.2, 1.5, 4.7, 2.8, 6.1 },
&.{ 10.0, 20.0, 15.0, 25.0 },
&.{ 100.5, 200.3, 50.1, 75.8, 150.2, 300.0 },
};
for (lotes, 1..) |lote, i| {
// Reset da arena entre lotes — libera memória do lote anterior
_ = arena.reset(.retain_capacity);
const stats = try processarLote(arena.allocator(), lote);
std.debug.print("Lote {d}: média={d:.2}, min={d:.2}, max={d:.2}, total={d:.2}\n", .{
i, stats.media, stats.min, stats.max, stats.total,
});
}
}
O reset(.retain_capacity) libera todas as alocações mas mantém a memória reservada, evitando idas ao sistema operacional nos próximos lotes.
Arena Scoped: Padrão de Escopo Temporário
Uma técnica poderosa é criar sub-escopos com arenas temporárias:
const std = @import("std");
fn operacaoCompleta(permanent_alloc: std.mem.Allocator) ![]u8 {
// Arena temporária para cálculos intermediários
var temp_arena = std.heap.ArenaAllocator.init(permanent_alloc);
defer temp_arena.deinit();
const temp = temp_arena.allocator();
// Dados intermediários — serão liberados automaticamente
const buffer1 = try temp.alloc(u8, 1000);
const buffer2 = try temp.alloc(u8, 2000);
_ = buffer2;
for (buffer1, 0..) |*b, i| {
b.* = @intCast(i % 256);
}
// Resultado final — alocado no allocator permanente
const resultado = try permanent_alloc.alloc(u8, 10);
@memcpy(resultado, buffer1[0..10]);
return resultado;
// temp_arena.deinit() libera buffer1 e buffer2 automaticamente
// resultado sobrevive porque foi alocado no permanent_alloc
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const resultado = try operacaoCompleta(gpa.allocator());
defer gpa.allocator().free(resultado);
std.debug.print("Resultado: {any}\n", .{resultado});
}
Melhores Práticas com Arena Allocator
- Use arena por unidade de trabalho — Uma requisição HTTP, um frame de jogo, um lote de processamento
- Prefira
reset(.retain_capacity)em loops — Evita syscalls repetidas - Combine com GPA para debugging — Passe GPA como backing allocator durante desenvolvimento
- Não libere individualmente — O objetivo da arena é liberar tudo junto;
free()individual é no-op
Resumo
| Padrão | Quando Usar |
|---|---|
| Arena por requisição | Servidores HTTP, processamento de mensagens |
| Arena por lote | Pipeline de dados, ETL |
| Arena temporária | Cálculos intermediários dentro de uma operação |
| Arena com reset | Loops de processamento com padrão repetitivo |
No próximo artigo, vamos aprender a criar custom allocators para necessidades específicas da sua aplicação.
Leitura Complementar
- Tipos de Allocators (Artigo 2) — Artigo anterior
- Performance com Zig — Otimizações que se beneficiam de arenas
- Desenvolvimento Web com Zig — Arenas em servidores HTTP