std.MultiArrayList — Estrutura de Arrays (SoA)
O std.MultiArrayList implementa o padrão Structure of Arrays (SoA), onde uma coleção de structs é armazenada como arrays separados para cada campo, em vez de um array de structs (AoS). Este layout melhora drasticamente o desempenho de cache quando o código acessa apenas alguns campos de cada vez, sendo uma peça central do design orientado a dados (data-oriented design) que o Zig promove.
Visão Geral
const std = @import("std");
const MultiArrayList = std.MultiArrayList;
Por que SoA?
Considere uma struct Particula com posição, velocidade e cor. Se você iterar sobre todas as partículas atualizando apenas a posição, com AoS (Array of Structs) cada acesso carrega para o cache os campos de velocidade e cor desnecessariamente. Com SoA, os campos de posição ficam contíguos na memória, maximizando a utilização do cache.
Tipo e Assinatura
pub fn MultiArrayList(comptime T: type) type
O tipo T deve ser uma struct. O MultiArrayList gera automaticamente arrays separados para cada campo.
Funções Principais
Criação e Destruição
// Cria lista vazia
pub fn init() MultiArrayList(T) // não requer allocator no init
// Libera memória
pub fn deinit(self: *Self, allocator: Allocator) void
Inserção e Remoção
// Adiciona um elemento (struct completa)
pub fn append(self: *Self, allocator: Allocator, elem: T) Allocator.Error!void
// Remove o último elemento
pub fn pop(self: *Self) void
// Remove em índice (preserva ordem)
pub fn orderedRemove(self: *Self, index: usize) void
// Remove por troca (O(1))
pub fn swapRemove(self: *Self, index: usize) void
Acesso aos Dados
// Acesso ao slice de um campo específico
pub fn items(self: Self, comptime field: FieldEnum) []FieldType
// Número de elementos
pub fn len(self: Self) usize
// Acessa um elemento completo como struct
pub fn get(self: Self, index: usize) T
// Define um elemento completo
pub fn set(self: *Self, index: usize, elem: T) void
Exemplo 1: Sistema de Partículas
const std = @import("std");
const Particula = struct {
x: f32,
y: f32,
vx: f32,
vy: f32,
vida: f32,
ativa: bool,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var particulas = std.MultiArrayList(Particula){};
defer particulas.deinit(allocator);
// Cria partículas
for (0..5) |i| {
const fi: f32 = @floatFromInt(i);
try particulas.append(allocator, .{
.x = fi * 10.0,
.y = 0.0,
.vx = 1.0,
.vy = fi * 0.5,
.vida = 100.0,
.ativa = true,
});
}
// Atualiza apenas posições — acesso SoA eficiente
const xs = particulas.items(.x);
const ys = particulas.items(.y);
const vxs = particulas.items(.vx);
const vys = particulas.items(.vy);
for (xs, ys, vxs, vys) |*x, *y, vx, vy| {
x.* += vx;
y.* += vy;
}
// Exibe resultados
const stdout = std.io.getStdOut().writer();
try stdout.writeAll("Partículas após atualização:\n");
for (0..particulas.len) |i| {
const p = particulas.get(i);
try stdout.print(" P{d}: pos=({d:.1}, {d:.1}) vida={d:.0}\n", .{
i, p.x, p.y, p.vida,
});
}
}
Exemplo 2: Entidades de Jogo
const std = @import("std");
const Entidade = struct {
nome_id: u32,
pos_x: f32,
pos_y: f32,
saude: i32,
dano: i32,
tipo: Tipo,
const Tipo = enum { jogador, inimigo, npc };
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var entidades = std.MultiArrayList(Entidade){};
defer entidades.deinit(allocator);
try entidades.append(allocator, .{
.nome_id = 1, .pos_x = 0, .pos_y = 0,
.saude = 100, .dano = 10, .tipo = .jogador,
});
try entidades.append(allocator, .{
.nome_id = 2, .pos_x = 5, .pos_y = 3,
.saude = 50, .dano = 15, .tipo = .inimigo,
});
try entidades.append(allocator, .{
.nome_id = 3, .pos_x = -2, .pos_y = 7,
.saude = 30, .dano = 20, .tipo = .inimigo,
});
// Aplica dano a todos os inimigos (acessa apenas tipo e saude)
const tipos = entidades.items(.tipo);
const saudes = entidades.items(.saude);
for (tipos, saudes) |tipo, *saude| {
if (tipo == .inimigo) {
saude.* -= 10;
}
}
const stdout = std.io.getStdOut().writer();
try stdout.writeAll("Estado das entidades:\n");
for (0..entidades.len) |i| {
const e = entidades.get(i);
try stdout.print(" ID={d} tipo={s} saude={d}\n", .{
e.nome_id, @tagName(e.tipo), e.saude,
});
}
}
Exemplo 3: Benchmark SoA vs AoS Conceitual
const std = @import("std");
const Dados = struct {
valor_quente: u64, // campo acessado frequentemente
payload_frio: [56]u8, // campo raramente acessado (56 bytes)
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var soa = std.MultiArrayList(Dados){};
defer soa.deinit(allocator);
// Preenche com dados
for (0..1000) |i| {
try soa.append(allocator, .{
.valor_quente = i,
.payload_frio = [_]u8{0} ** 56,
});
}
// Soma apenas o campo quente — SoA carrega apenas 8KB (1000 * 8 bytes)
// Em AoS, carregaria 64KB (1000 * 64 bytes) para o cache
var soma: u64 = 0;
for (soa.items(.valor_quente)) |v| {
soma += v;
}
const stdout = std.io.getStdOut().writer();
try stdout.print("Soma dos valores quentes: {d}\n", .{soma});
try stdout.print("Elementos: {d}\n", .{soa.len});
}
Quando Usar MultiArrayList
- Iteração sobre poucos campos: Quando o loop acessa 1-2 campos de uma struct grande
- Dados numéricos em massa: Simulações físicas, gráficos, processamento de sinais
- SIMD: O layout contíguo facilita vetorização automática pelo compilador
- Structs com campos grandes: Quando structs têm campos raramente usados juntos
Módulos Relacionados
- std.ArrayList — Array dinâmico tradicional (AoS)
- std.BoundedArray — Array de tamanho fixo
- std.mem.Allocator — Interface de alocação