Benchmarking e Performance em Zig

Introdução

Medir performance é essencial para código de sistemas. Zig oferece ferramentas na biblioteca padrão para medição de tempo com alta resolução, e o sistema de testes pode ser usado para benchmarks simples. Para profiling avançado, Zig é compatível com ferramentas externas como perf e Valgrind.

Para testes em geral, veja Testes Unitários Básicos. Para otimização com SIMD, consulte o artigo Zig vs Assembly.

Pré-requisitos

Medição de Tempo Básica

const std = @import("std");

pub fn main() !void {
    var timer = try std.time.Timer.start();

    // Código a ser medido
    var soma: u64 = 0;
    for (0..10_000_000) |i| {
        soma += i;
    }

    const elapsed = timer.read();

    std.debug.print("Resultado: {}\n", .{soma});
    std.debug.print("Tempo: {}ns ({d:.3}ms)\n", .{
        elapsed,
        @as(f64, @floatFromInt(elapsed)) / 1_000_000.0,
    });
}

Benchmark com Múltiplas Iterações

const std = @import("std");

fn benchmark(comptime func: anytype, args: anytype, comptime iteracoes: usize) struct {
    media_ns: u64,
    min_ns: u64,
    max_ns: u64,
} {
    var min: u64 = std.math.maxInt(u64);
    var max: u64 = 0;
    var total: u64 = 0;

    for (0..iteracoes) |_| {
        var timer = std.time.Timer.start() catch unreachable;
        const resultado = @call(.never_inline, func, args);
        const elapsed = timer.read();
        std.mem.doNotOptimizeAway(&resultado);

        total += elapsed;
        if (elapsed < min) min = elapsed;
        if (elapsed > max) max = elapsed;
    }

    return .{
        .media_ns = total / iteracoes,
        .min_ns = min,
        .max_ns = max,
    };
}

fn somaIterativa(n: u64) u64 {
    var soma: u64 = 0;
    for (0..n) |i| {
        soma += i;
    }
    return soma;
}

fn somaFormula(n: u64) u64 {
    return n * (n - 1) / 2;
}

pub fn main() !void {
    const n: u64 = 1_000_000;
    const iter: usize = 100;

    const b1 = benchmark(somaIterativa, .{n}, iter);
    const b2 = benchmark(somaFormula, .{n}, iter);

    std.debug.print("Soma iterativa: média={}ns, min={}ns, max={}ns\n", .{
        b1.media_ns, b1.min_ns, b1.max_ns,
    });
    std.debug.print("Soma fórmula:   média={}ns, min={}ns, max={}ns\n", .{
        b2.media_ns, b2.min_ns, b2.max_ns,
    });
}

Prevenir Otimização Espúria

O compilador pode eliminar código cujo resultado não é usado. Use doNotOptimizeAway para prevenir isso:

const std = @import("std");

fn benchmarkHash(dados: []const u8, iteracoes: usize) u64 {
    var timer = std.time.Timer.start() catch unreachable;

    for (0..iteracoes) |_| {
        const hash = std.hash.Wyhash.hash(0, dados);
        std.mem.doNotOptimizeAway(&hash); // Impede o compilador de eliminar
    }

    return timer.read();
}

Benchmark em Testes

test "benchmark busca linear vs binária" {
    const dados = blk: {
        var arr: [10000]i32 = undefined;
        for (&arr, 0..) |*v, i| v.* = @intCast(i * 2);
        break :blk arr;
    };

    const alvo: i32 = 9998;

    // Busca linear
    {
        var timer = try std.time.Timer.start();
        for (0..1000) |_| {
            const resultado = std.mem.indexOf(i32, &dados, &.{alvo});
            std.mem.doNotOptimizeAway(&resultado);
        }
        const elapsed = timer.read();
        std.debug.print("\nBusca linear: {}ns/op\n", .{elapsed / 1000});
    }

    // Busca binária
    {
        var timer = try std.time.Timer.start();
        for (0..1000) |_| {
            const resultado = std.sort.binarySearch(i32, alvo, &dados, {}, struct {
                fn f(_: void, a: i32, b: i32) std.math.Order {
                    return std.math.order(a, b);
                }
            }.f);
            std.mem.doNotOptimizeAway(&resultado);
        }
        const elapsed = timer.read();
        std.debug.print("Busca binária: {}ns/op\n", .{elapsed / 1000});
    }
}

Benchmark de Allocators

fn benchmarkAllocator(
    allocator: std.mem.Allocator,
    comptime nome: []const u8,
    iteracoes: usize,
) !void {
    var timer = try std.time.Timer.start();

    for (0..iteracoes) |_| {
        const buf = try allocator.alloc(u8, 1024);
        allocator.free(buf);
    }

    const elapsed = timer.read();
    std.debug.print("{s}: {}ns/op ({} operações)\n", .{
        nome,
        elapsed / iteracoes,
        iteracoes,
    });
}

pub fn main() !void {
    const iter = 100_000;

    // GPA
    {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        try benchmarkAllocator(gpa.allocator(), "GPA", iter);
    }

    // Page allocator
    try benchmarkAllocator(std.heap.page_allocator, "Page", iter);

    // Arena
    {
        var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
        defer arena.deinit();
        try benchmarkAllocator(arena.allocator(), "Arena", iter);
    }
}

Veja ArenaAllocator e GeneralPurposeAllocator.

Usando Ferramentas Externas

perf (Linux)

# Compilar com símbolos de debug
zig build -Doptimize=ReleaseFast

# Profiling
perf stat ./zig-out/bin/meu-programa
perf record -g ./zig-out/bin/meu-programa
perf report

Valgrind (Callgrind)

zig build -Doptimize=ReleaseFast

valgrind --tool=callgrind ./zig-out/bin/meu-programa
kcachegrind callgrind.out.*

Hyperfine (benchmarks de CLI)

hyperfine './zig-out/bin/implementacao_a' './zig-out/bin/implementacao_b'

Boas Práticas

  1. Compile com ReleaseFast para benchmarks: zig build -Doptimize=ReleaseFast
  2. Faça warmup: Ignore as primeiras iterações
  3. Use doNotOptimizeAway: Previna eliminação de código pelo compilador
  4. Meça múltiplas vezes: Use min/max/média para resultado confiável
  5. Isole o código medido: Evite medir setup/teardown
  6. Compare com baseline: Sempre compare com uma referência
  7. Use hardware consistente: Desative turbo boost para resultados reproduzíveis

Conclusão

Benchmarking em Zig é direto — use std.time.Timer para medições, doNotOptimizeAway para prevenir otimização espúria, e compile com ReleaseFast para resultados realistas. Para profiling mais detalhado, integre com ferramentas externas como perf e Valgrind.

Para mais sobre performance, veja o artigo Zig vs Assembly e Quando Usar Zig. Para testes, consulte Testes Unitários.

Continue aprendendo Zig

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