Ferramentas de Profiling para Zig: perf, Tracy e Valgrind

Otimizacao sem profiling e adivinhacao. Profiling mostra exatamente onde seu programa gasta tempo, quanta memoria usa e onde estao os gargalos. Neste artigo, exploramos as principais ferramentas de profiling para codigo Zig.

Continuacao do artigo sobre SIMD e vetorizacao em Zig.

A Regra de Ouro: Meça Antes de Otimizar

Donald Knuth disse: “Otimizacao prematura e a raiz de todo mal.” A versao moderna dessa frase e:

  1. Faca funcionar — codigo correto primeiro
  2. Meça — profile para encontrar o gargalo real
  3. Otimize o hotspot — foque nos 20% que causam 80% do tempo
  4. Meça de novo — confirme que a otimizacao funcionou

Linux perf

perf e a ferramenta de profiling mais poderosa no Linux. Ela usa hardware performance counters da CPU para medir com precisao e baixo overhead.

Compilando para Profiling

# Compilar com simbolos de debug + otimizacoes
zig build -Doptimize=ReleaseSafe

# ReleaseSafe mantem simbolos de debug e bounds checking
# mas aplica otimizacoes do compilador

Profiling Basico com perf

# Gravar perfil de execucao
perf record -g ./zig-out/bin/minha-app

# Visualizar resultados
perf report

# Estatisticas rapidas (sem gravacao)
perf stat ./zig-out/bin/minha-app

Saida tipica do perf stat:

Performance counter stats for './zig-out/bin/minha-app':

          1,234.56 msec task-clock
                42      context-switches
                 3      cpu-migrations
            12,345      page-faults
     4,567,890,123      cycles
     8,901,234,567      instructions       #    1.95  insn per cycle
     1,234,567,890      branches
        12,345,678      branch-misses      #    1.00% of all branches
       345,678,901      L1-dcache-loads
        23,456,789      L1-dcache-load-misses  #    6.79% of all L1-dcache

Analisando Cache Misses

# Medir cache misses especificamente
perf stat -e L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses \
    ./zig-out/bin/minha-app

# Encontrar funcoes com mais cache misses
perf record -e cache-misses -g ./zig-out/bin/minha-app
perf report --sort=dso,symbol

Flame Graphs

Flame graphs mostram visualmente onde seu programa gasta tempo:

# Gravar dados
perf record -F 99 -g ./zig-out/bin/minha-app

# Gerar flame graph (requer FlameGraph tools)
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

# Abrir no navegador
xdg-open flamegraph.svg

Tracy Profiler

Tracy e um profiler em tempo real, ideal para games e aplicacoes interativas. O Zig tem integracao nativa via std.debug.Trace.

Integrando Tracy no Codigo Zig

const std = @import("std");
const tracy = @import("tracy");

pub fn main() void {
    // Zona de profiling com nome
    {
        const zone = tracy.trace(@src());
        defer zone.end();

        // Codigo a ser medido
        processarDados();
    }

    // Zona com cor customizada
    {
        const zone = tracy.trace(@src());
        defer zone.end();
        zone.setColor(0xFF0000); // Vermelho
        zone.setName("Renderizar Frame");

        renderizar();
    }
}

fn processarDados() void {
    const zone = tracy.trace(@src());
    defer zone.end();

    // Marcar alocacoes de memoria
    const allocator = tracy.tracyAllocator(std.heap.page_allocator);
    const dados = allocator.alloc(u8, 1024) catch return;
    defer allocator.free(dados);

    // ... processar ...
}

fn renderizar() void {
    const zone = tracy.trace(@src());
    defer zone.end();

    // Plotar valor no timeline do Tracy
    tracy.plot("FPS", 60.0);
    tracy.plot("Memoria MB", 128.5);
}

Configurando Tracy no build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Flag para habilitar/desabilitar Tracy
    const enable_tracy = b.option(bool, "tracy", "Habilitar Tracy profiler") orelse false;

    const exe = b.addExecutable(.{
        .name = "minha-app",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    if (enable_tracy) {
        const tracy_dep = b.dependency("tracy", .{
            .target = target,
            .optimize = optimize,
        });
        exe.root_module.addImport("tracy", tracy_dep.module("tracy"));
        exe.linkLibC();
    }

    b.installArtifact(exe);
}

Valgrind

Valgrind e essencial para encontrar problemas de memoria e analisar comportamento de cache.

Callgrind: Profiling de CPU

# Executar com Callgrind
valgrind --tool=callgrind ./zig-out/bin/minha-app

# Visualizar com KCachegrind
kcachegrind callgrind.out.*

Cachegrind: Analise de Cache

# Simular comportamento de cache
valgrind --tool=cachegrind ./zig-out/bin/minha-app

# Saida mostra misses por funcao:
# ==1234== D   refs:      12,345,678
# ==1234== D1  misses:       234,567  (1.90%)
# ==1234== LLd misses:        12,345  (0.10%)

# Anotar codigo fonte com contagens de cache
cg_annotate cachegrind.out.* src/main.zig

Massif: Profiling de Memoria

# Rastrear uso de memoria ao longo do tempo
valgrind --tool=massif ./zig-out/bin/minha-app

# Visualizar
ms_print massif.out.*

Instrumentacao Manual em Zig

Timer Embutido para Profiling Leve

const std = @import("std");

/// Profiler leve embutido no codigo
const Profiler = struct {
    const MAX_ZONAS = 64;

    const Zona = struct {
        nome: []const u8,
        total_ns: u64 = 0,
        chamadas: u64 = 0,
        min_ns: u64 = std.math.maxInt(u64),
        max_ns: u64 = 0,
    };

    zonas: [MAX_ZONAS]Zona = undefined,
    num_zonas: usize = 0,

    pub fn registrarZona(self: *Profiler, nome: []const u8) usize {
        const id = self.num_zonas;
        self.zonas[id] = .{ .nome = nome };
        self.num_zonas += 1;
        return id;
    }

    pub fn iniciarMedicao(_: *Profiler) std.time.Timer {
        return std.time.Timer.start() catch unreachable;
    }

    pub fn finalizarMedicao(self: *Profiler, id: usize, timer: std.time.Timer) void {
        const elapsed = timer.read();
        self.zonas[id].total_ns += elapsed;
        self.zonas[id].chamadas += 1;
        self.zonas[id].min_ns = @min(self.zonas[id].min_ns, elapsed);
        self.zonas[id].max_ns = @max(self.zonas[id].max_ns, elapsed);
    }

    pub fn relatorio(self: *Profiler) void {
        std.debug.print("\n=== Relatorio de Performance ===\n", .{});
        std.debug.print("{s:<30} {s:>10} {s:>10} {s:>10} {s:>10} {s:>10}\n", .{
            "Zona", "Chamadas", "Total(ms)", "Media(us)", "Min(us)", "Max(us)",
        });
        std.debug.print("{s:-<90}\n", .{""});

        for (self.zonas[0..self.num_zonas]) |zona| {
            const total_ms = zona.total_ns / std.time.ns_per_ms;
            const media_us = if (zona.chamadas > 0)
                zona.total_ns / zona.chamadas / std.time.ns_per_us
            else
                0;
            const min_us = zona.min_ns / std.time.ns_per_us;
            const max_us = zona.max_ns / std.time.ns_per_us;

            std.debug.print("{s:<30} {d:>10} {d:>10} {d:>10} {d:>10} {d:>10}\n", .{
                zona.nome, zona.chamadas, total_ms, media_us, min_us, max_us,
            });
        }
    }
};

var profiler = Profiler{};

// Uso global
const ZONA_PROCESSAR = blk: {
    break :blk 0;
};

fn processarComProfile(dados: []const u8) void {
    const timer = profiler.iniciarMedicao();
    defer profiler.finalizarMedicao(0, timer);

    // ... codigo real ...
    _ = dados;
}

Identificando Hotspots Comuns

1. Alocacoes Excessivas

// LENTO: aloca e desaloca em cada iteracao
fn processarLento(items: []const Item, allocator: std.mem.Allocator) !void {
    for (items) |item| {
        const resultado = try allocator.alloc(u8, item.tamanho);
        defer allocator.free(resultado);
        // ... processar ...
    }
}

// RAPIDO: reutilizar buffer
fn processarRapido(items: []const Item, allocator: std.mem.Allocator) !void {
    var buffer = std.ArrayList(u8).init(allocator);
    defer buffer.deinit();

    for (items) |item| {
        buffer.clearRetainingCapacity();
        try buffer.resize(item.tamanho);
        // ... processar usando buffer.items ...
    }
}

2. Copias Desnecessarias

// LENTO: copia a struct inteira
fn processarStruct(s: MinhaStructGrande) void {
    // s e uma copia — pode ser 256+ bytes
    _ = s;
}

// RAPIDO: passa por referencia
fn processarStructRef(s: *const MinhaStructGrande) void {
    // s e um ponteiro — 8 bytes
    _ = s;
}

3. Branch Mispredictions

// LENTO: branch imprevisivel
fn filtrarSlow(dados: []const u32) u64 {
    var soma: u64 = 0;
    for (dados) |v| {
        if (v > 128) { // Branch imprevisivel para dados aleatorios
            soma += v;
        }
    }
    return soma;
}

// RAPIDO: branchless com bitmask
fn filtrarFast(dados: []const u32) u64 {
    var soma: u64 = 0;
    for (dados) |v| {
        // Sem branch: mascara e 0 ou 0xFFFFFFFF
        const mask: u32 = if (v > 128) 0xFFFFFFFF else 0;
        soma += v & mask;
    }
    return soma;
}

Conclusao

Profiling nao e opcional — e o unico caminho confiavel para otimizacao eficaz. Use perf para analise de CPU e cache no Linux, Tracy para profiling em tempo real de aplicacoes interativas, e Valgrind para analise profunda de memoria. Combine essas ferramentas com benchmarking robusto e voce tera uma visao completa de onde seu codigo Zig pode melhorar.

Proximo Artigo

No Artigo 5: Otimizacao Real, aplicamos todas as tecnicas desta serie em um estudo de caso real, com medicoes antes e depois.

Conteudo Relacionado

Continue aprendendo Zig

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