O gargalo de performance numero um em software moderno nao e a CPU — e o acesso a memoria. Uma cache miss pode custar 100x mais que uma operacao aritmetica. Neste artigo, exploramos como estruturar dados em Zig para maximizar hits de cache e minimizar acessos lentos a RAM.
Continuacao do artigo sobre benchmarking em Zig.
Hierarquia de Memoria
Latencia de Acesso
| Nivel | Tamanho Tipico | Latencia | Equivalente |
|---|---|---|---|
| Registrador | ~1 KB | ~0.5 ns | 1 passo |
| L1 Cache | 32-64 KB | ~1 ns | 2 passos |
| L2 Cache | 256 KB - 1 MB | ~4 ns | 8 passos |
| L3 Cache | 4-32 MB | ~12 ns | 24 passos |
| RAM | 8-64 GB | ~100 ns | 200 passos |
| SSD | 256 GB+ | ~100 μs | 200.000 passos |
Uma cache miss de L1 para RAM custa 100x mais. Otimizar acesso a cache e a otimizacao mais impactante que voce pode fazer.
Array of Structs vs Struct of Arrays
O Problema: AoS (Array of Structs)
const std = @import("std");
// AoS — layout tradicional (orientado a objetos)
const Particula = struct {
x: f32, // 4 bytes
y: f32, // 4 bytes
z: f32, // 4 bytes
vx: f32, // 4 bytes
vy: f32, // 4 bytes
vz: f32, // 4 bytes
massa: f32, // 4 bytes
cor: u32, // 4 bytes — total: 32 bytes
};
// Para atualizar posicoes, precisamos ler TODA a struct (32 bytes)
// mesmo que so usemos x, y, z, vx, vy, vz (24 bytes)
fn atualizarPosicoesAoS(particulas: []Particula, dt: f32) void {
for (particulas) |*p| {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
}
}
A Solucao: SoA (Struct of Arrays)
// SoA — layout orientado a dados
const Particulas = struct {
x: []f32,
y: []f32,
z: []f32,
vx: []f32,
vy: []f32,
vz: []f32,
massa: []f32,
cor: []u32,
len: usize,
pub fn init(allocator: std.mem.Allocator, n: usize) !Particulas {
return .{
.x = try allocator.alloc(f32, n),
.y = try allocator.alloc(f32, n),
.z = try allocator.alloc(f32, n),
.vx = try allocator.alloc(f32, n),
.vy = try allocator.alloc(f32, n),
.vz = try allocator.alloc(f32, n),
.massa = try allocator.alloc(f32, n),
.cor = try allocator.alloc(u32, n),
.len = n,
};
}
pub fn deinit(self: Particulas, allocator: std.mem.Allocator) void {
allocator.free(self.x);
allocator.free(self.y);
allocator.free(self.z);
allocator.free(self.vx);
allocator.free(self.vy);
allocator.free(self.vz);
allocator.free(self.massa);
allocator.free(self.cor);
}
};
// Agora cada array e contiguous — o prefetcher da CPU adora isso
fn atualizarPosicoesSoA(p: *Particulas, dt: f32) void {
for (0..p.len) |i| {
p.x[i] += p.vx[i] * dt;
p.y[i] += p.vy[i] * dt;
p.z[i] += p.vz[i] * dt;
}
}
Benchmark: AoS vs SoA
test "benchmark AoS vs SoA" {
const N = 1_000_000;
const dt: f32 = 0.016; // 60 FPS
const allocator = std.testing.allocator;
// === AoS ===
const particulas_aos = try allocator.alloc(Particula, N);
defer allocator.free(particulas_aos);
for (particulas_aos) |*p| p.* = .{
.x = 0, .y = 0, .z = 0,
.vx = 1, .vy = 2, .vz = 3,
.massa = 1, .cor = 0xFFFFFF,
};
// Warmup
for (0..10) |_| atualizarPosicoesAoS(particulas_aos, dt);
var timer_aos = try std.time.Timer.start();
for (0..100) |_| atualizarPosicoesAoS(particulas_aos, dt);
const tempo_aos = timer_aos.read();
// === SoA ===
var particulas_soa = try Particulas.init(allocator, N);
defer particulas_soa.deinit(allocator);
for (0..N) |i| {
particulas_soa.vx[i] = 1;
particulas_soa.vy[i] = 2;
particulas_soa.vz[i] = 3;
}
// Warmup
for (0..10) |_| atualizarPosicoesSoA(&particulas_soa, dt);
var timer_soa = try std.time.Timer.start();
for (0..100) |_| atualizarPosicoesSoA(&particulas_soa, dt);
const tempo_soa = timer_soa.read();
std.debug.print(
\\AoS: {d} ms
\\SoA: {d} ms
\\Speedup: {d:.2}x
\\
, .{
tempo_aos / std.time.ns_per_ms,
tempo_soa / std.time.ns_per_ms,
@as(f64, @floatFromInt(tempo_aos)) / @as(f64, @floatFromInt(tempo_soa)),
});
}
MultiArrayList do Zig
O Zig oferece std.MultiArrayList que implementa SoA automaticamente:
const std = @import("std");
const Entidade = struct {
x: f32,
y: f32,
saude: u16,
tipo: enum(u8) { jogador, inimigo, projetil },
ativo: bool,
};
test "MultiArrayList — SoA automatico" {
var lista = std.MultiArrayList(Entidade){};
defer lista.deinit(std.testing.allocator);
// Adicionar entidades
try lista.append(std.testing.allocator, .{
.x = 10, .y = 20, .saude = 100, .tipo = .jogador, .ativo = true,
});
try lista.append(std.testing.allocator, .{
.x = 50, .y = 30, .saude = 50, .tipo = .inimigo, .ativo = true,
});
// Acessar como SoA — cada campo e um slice contiguous
const slice = lista.slice();
const xs = slice.items(.x);
const ys = slice.items(.y);
const saudes = slice.items(.saude);
// Atualizar apenas posicoes — acesso contiguous, cache-friendly
for (xs, ys) |*x, *y| {
x.* += 1.0;
y.* += 0.5;
}
try std.testing.expectApproxEqAbs(@as(f32, 11.0), xs[0], 0.001);
try std.testing.expectApproxEqAbs(@as(f32, 51.0), xs[1], 0.001);
// Filtrar por saude — acessa apenas o array de saude
var vivos: u32 = 0;
for (saudes) |s| {
if (s > 0) vivos += 1;
}
try std.testing.expectEqual(@as(u32, 2), vivos);
}
Alinhamento e Padding
Entendendo Padding de Structs
const std = @import("std");
// Struct com padding desperdicado
const MalAlinhada = struct {
a: bool, // 1 byte + 3 bytes padding
b: u32, // 4 bytes
c: bool, // 1 byte + 1 byte padding
d: u16, // 2 bytes
// Total: 12 bytes (mas so 8 bytes de dados uteis!)
};
// Struct otimizada — campos ordenados por tamanho decrescente
const BemAlinhada = struct {
b: u32, // 4 bytes
d: u16, // 2 bytes
a: bool, // 1 byte
c: bool, // 1 byte
// Total: 8 bytes — zero padding desperdicado!
};
test "tamanho de structs com padding" {
std.debug.print("MalAlinhada: {d} bytes\n", .{@sizeOf(MalAlinhada)});
std.debug.print("BemAlinhada: {d} bytes\n", .{@sizeOf(BemAlinhada)});
// Packed struct remove todo padding
const Packed = packed struct {
a: bool,
b: u32,
c: bool,
d: u16,
};
std.debug.print("Packed: {d} bytes\n", .{@sizeOf(Packed)});
}
Alinhamento Explicito para Cache Lines
// Alinhar estrutura a cache line de 64 bytes
// para evitar false sharing em codigo multi-threaded
const ContadorThread = struct {
valor: std.atomic.Value(u64) align(64) = std.atomic.Value(u64).init(0),
// 56 bytes de padding implicito ate proxima cache line
};
test "contadores alinhados evitam false sharing" {
var contadores: [8]ContadorThread = .{.{}} ** 8;
// Cada contador esta em sua propria cache line
// Threads podem incrementar sem interferencia
for (&contadores) |*c| {
_ = c.valor.fetchAdd(1, .monotonic);
}
}
Prefetching Manual
/// Prefetch de dados para o cache antes de usa-los
fn processarComPrefetch(dados: []const u32) u64 {
var soma: u64 = 0;
const prefetch_distance = 16; // 16 elementos a frente
for (dados, 0..) |v, i| {
// Prefetch dados que serao usados em breve
if (i + prefetch_distance < dados.len) {
@prefetch(&dados[i + prefetch_distance], .{
.rw = .read,
.locality = 3, // Manter no cache L1
});
}
soma += v;
}
return soma;
}
Hot/Cold Splitting
Separe dados acessados frequentemente dos acessados raramente:
// Dados quentes — acessados em todo frame
const EntidadeHot = struct {
x: f32,
y: f32,
velocidade_x: f32,
velocidade_y: f32,
};
// Dados frios — acessados raramente
const EntidadeCold = struct {
nome: []const u8,
sprite_path: []const u8,
criado_em: i64,
ultima_interacao: i64,
descricao: []const u8,
};
const Mundo = struct {
// Arrays quentes — cache-friendly para update loop
hot: std.MultiArrayList(EntidadeHot),
// Arrays frios — nao poluem o cache no loop principal
cold: std.MultiArrayList(EntidadeCold),
pub fn update(self: *Mundo, dt: f32) void {
// So toca dados quentes — muito cache-friendly
const slice = self.hot.slice();
const xs = slice.items(.x);
const ys = slice.items(.y);
const vxs = slice.items(.velocidade_x);
const vys = slice.items(.velocidade_y);
for (xs, ys, vxs, vys) |*x, *y, vx, vy| {
x.* += vx * dt;
y.* += vy * dt;
}
}
};
Conclusao
Codigo cache-friendly pode ser 10-100x mais rapido que codigo com acesso aleatorio a memoria. As tecnicas-chave sao: usar SoA em vez de AoS para loops que acessam poucos campos, manter dados quentes separados dos frios, alinhar estruturas corretamente e preferir acesso sequencial. O std.MultiArrayList do Zig torna a implementacao de SoA trivial.
Proximo Artigo
No Artigo 3: SIMD e Vetorizacao, exploramos como processar multiplos dados simultaneamente usando instrucoes vetorizadas.
Conteudo Relacionado
- Benchmarking em Zig — Artigo anterior
- SIMD e Vetorizacao — Proximo artigo
- Gerenciamento de Memoria — Fundamentos
- Masterclass Memoria — Serie avancada
- Estruturas de Dados em Zig — Data structures