Ferramentas de Profiling em Zig — Análise de Performance e Otimização

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

  1. Meça antes de otimizar: Use profiling para identificar hotspots reais
  2. Otimize layout de dados: Minimize padding e maximize cache hits
  3. Use SIMD: Para operações vetoriais, ganho de 4-8x
  4. Prefira iteração a recursão: Evite overhead de chamada de função
  5. Arena Allocator para temporários: Elimine overhead de alocação/liberação individual
  6. 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.

Continue aprendendo Zig

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