Ferramentas de Profiling em Zig — Análise de Performance e Otimização
Performance é um dos pilares do Zig, e o ecossistema oferece ferramentas poderosas para medir, analisar e otimizar código. Desde profilers de sistema como perf e Tracy até técnicas de benchmark integradas, este guia cobre tudo o que você precisa para extrair a máxima performance dos seus programas Zig.
Profiling com perf (Linux)
O perf é a ferramenta de profiling padrão do Linux e funciona perfeitamente com binários Zig:
# Compilar com informações de debug (mesmo em release)
zig build -Doptimize=ReleaseFast
# Gravar profile
perf record -g ./zig-out/bin/minha-app
# Analisar resultados
perf report
# Flamegraph
perf script | stackcollapse-perf.pl | flamegraph.pl > perfil.svg
Análise de Cache
# Cache misses
perf stat -e cache-misses,cache-references,instructions,cycles ./zig-out/bin/minha-app
# Profiling detalhado de cache
perf stat -e L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses ./zig-out/bin/minha-app
Branch Prediction
perf stat -e branch-misses,branch-instructions ./zig-out/bin/minha-app
Tracy — Profiler Visual em Tempo Real
O Tracy é um profiler visual de alta performance que tem excelente suporte a Zig:
Integração com Zig
const tracy = @import("tracy");
pub fn main() void {
// Zona de profiling
const zone = tracy.trace(@src());
defer zone.end();
processarDados();
renderizar();
}
fn processarDados() void {
const zone = tracy.trace(@src());
defer zone.end();
zone.setName("Processamento de Dados");
zone.setColor(0xFF0000); // Vermelho
// Código sendo analisado
for (0..1000) |i| {
_ = calcular(i);
}
zone.setValue(1000); // Valor customizado
}
fn renderizar() void {
const zone = tracy.trace(@src());
defer zone.end();
zone.setName("Renderização");
// Frames
tracy.frameMark();
}
Configuração no build.zig
const tracy_dep = b.dependency("tracy", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("tracy", tracy_dep.module("tracy"));
exe.linkLibrary(tracy_dep.artifact("tracy"));
Benchmark Integrado
Timer de Alta Resolução
const std = @import("std");
pub fn benchmark(comptime nome: []const u8, comptime func: anytype) void {
const warmup = 100;
const iteracoes = 10000;
// Warmup
for (0..warmup) |_| {
_ = @call(.never_inline, func, .{});
}
// Medição
var tempos: [iteracoes]u64 = undefined;
for (0..iteracoes) |i| {
const inicio = std.time.nanoTimestamp();
_ = @call(.never_inline, func, .{});
const fim = std.time.nanoTimestamp();
tempos[i] = @intCast(fim - inicio);
}
// Estatísticas
std.sort.pdq(u64, &tempos, {}, std.sort.asc(u64));
const mediana = tempos[iteracoes / 2];
const p99 = tempos[@as(usize, @intFromFloat(@as(f64, iteracoes) * 0.99))];
var soma: u128 = 0;
for (tempos) |t| soma += t;
const media = @as(u64, @intCast(soma / iteracoes));
std.debug.print("\n--- {s} ---\n", .{nome});
std.debug.print(" Média: {} ns\n", .{media});
std.debug.print(" Mediana: {} ns\n", .{mediana});
std.debug.print(" P99: {} ns\n", .{p99});
std.debug.print(" Min: {} ns\n", .{tempos[0]});
std.debug.print(" Max: {} ns\n", .{tempos[iteracoes - 1]});
}
// Uso
pub fn main() void {
benchmark("sorting 1000 elementos", struct {
fn run() void {
var dados: [1000]u32 = undefined;
for (&dados, 0..) |*d, i| d.* = @intCast(1000 - i);
std.sort.pdq(u32, &dados, {}, std.sort.asc(u32));
}
}.run);
}
Otimizações Específicas do Zig
Análise de Layout de Memória
fn analisarLayout(comptime T: type) void {
const info = @typeInfo(T);
if (info == .Struct) {
std.debug.print("Struct: {s}\n", .{@typeName(T)});
std.debug.print(" Tamanho: {} bytes\n", .{@sizeOf(T)});
std.debug.print(" Alinhamento: {} bytes\n", .{@alignOf(T)});
inline for (info.Struct.fields) |field| {
std.debug.print(" Campo '{s}': offset={}, size={}\n", .{
field.name,
@offsetOf(T, field.name),
@sizeOf(field.type),
});
}
}
}
// Identificar padding desnecessário
const IneficienteStruct = struct {
a: u8, // 1 byte + 7 padding
b: u64, // 8 bytes
c: u8, // 1 byte + 7 padding
d: u64, // 8 bytes
// Total: 32 bytes (16 bytes de padding!)
};
const EficienteStruct = struct {
b: u64, // 8 bytes
d: u64, // 8 bytes
a: u8, // 1 byte
c: u8, // 1 byte + 6 padding
// Total: 24 bytes (6 bytes de padding)
};
Otimização SIMD
fn somaVetorial(a: []const f32, b: []const f32, resultado: []f32) void {
const vec_len = 8; // AVX-256
var i: usize = 0;
while (i + vec_len <= a.len) : (i += vec_len) {
const va: @Vector(vec_len, f32) = a[i..][0..vec_len].*;
const vb: @Vector(vec_len, f32) = b[i..][0..vec_len].*;
resultado[i..][0..vec_len].* = va + vb;
}
// Resto escalar
while (i < a.len) : (i += 1) {
resultado[i] = a[i] + b[i];
}
}
Prefetch de Cache
fn buscarComPrefetch(dados: []const u64, indices: []const usize) u64 {
var soma: u64 = 0;
for (indices, 0..) |idx, i| {
// Prefetch do próximo acesso
if (i + 4 < indices.len) {
@prefetch(&dados[indices[i + 4]], .{
.rw = .read,
.locality = 1,
});
}
soma += dados[idx];
}
return soma;
}
Ferramentas Externas
Valgrind Cachegrind
valgrind --tool=cachegrind ./zig-out/bin/app
cg_annotate cachegrind.out.<pid>
Instruments (macOS)
# Time Profiler
xcrun xctrace record --template "Time Profiler" --launch ./app
# Allocations
xcrun xctrace record --template "Allocations" --launch ./app
Boas Práticas de Performance
- Meça antes de otimizar: Use profiling para identificar hotspots reais
- Otimize layout de dados: Minimize padding e maximize cache hits
- Use SIMD: Para operações vetoriais, ganho de 4-8x
- Prefira iteração a recursão: Evite overhead de chamada de função
- Arena Allocator para temporários: Elimine overhead de alocação/liberação individual
- Compile com ReleaseFast para benchmarks: Os resultados em Debug não são representativos
Próximos Passos
Explore as ferramentas de debug para resolver problemas encontrados durante profiling, os frameworks de teste para benchmark automatizado, e os alocadores customizados para otimização de memória. Veja como projetos como TigerBeetle e Bun alcançam performance excepcional.