std.MultiArrayList em Zig — Referência e Exemplos

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

Tutoriais e Receitas Relacionados

Continue aprendendo Zig

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