---
title: "Benchmarking em Zig: Medir Performance na Prática"
url: "https://ziglang.com.br/artigos/zig-benchmarking-medir-performance/"
markdown_url: "https://ziglang.com.br/artigos/zig-benchmarking-medir-performance.MD"
description: "Aprenda a medir performance em Zig com std.time.Timer, doNotOptimizeAway, hyperfine, perf stat, Tracy e comparativos entre release modes. Guia prático com exemplos."
date: "2026-05-12"
author: ""
---

# Benchmarking em Zig: Medir Performance na Prática

Aprenda a medir performance em Zig com std.time.Timer, doNotOptimizeAway, hyperfine, perf stat, Tracy e comparativos entre release modes. Guia prático com exemplos.


Medir performance de forma confiável é uma das habilidades mais valiosas para quem trabalha com programação de sistemas. Em Zig, a combinação de controle fino sobre a memória, [comptime](/artigos/comptime-zig-metaprogramacao/) e modos de compilação otimizados cria um ambiente ideal para benchmarks reprodutíveis — sem depender de frameworks externos.

Neste guia, vamos construir benchmarks práticos do zero, entender como evitar armadilhas do compilador e integrar ferramentas de profiling como Tracy para identificar gargalos reais.

## Por que Benchmark em Zig é Diferente

Diferente de linguagens com runtime pesado, Zig compila diretamente para código de máquina sem garbage collector, overhead de abstração ou JIT. Isso significa que seus benchmarks medem o código real — não artefatos do runtime. Mas também significa que o otimizador pode eliminar código que parece "sem efeito colateral", invalidando seus resultados.

A [biblioteca padrão](/stdlib/) de Zig oferece primitivas específicas para contornar esse problema, como veremos a seguir.

## Medindo Tempo com std.time.Timer

O ponto de partida para qualquer benchmark é a medição precisa de tempo. O `std.time.Timer` usa o relógio monotônico do sistema operacional:

```zig
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..1_000_000) |i| {
        soma += i;
    }

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

Execute com o modo de release desejado:

```bash
zig build-exe benchmark.zig -OReleaseFast
./benchmark
```

O `Timer` retorna nanosegundos, oferecendo resolução suficiente para microbenchmarks. Sempre imprima o resultado da computação para evitar que o compilador elimine o cálculo inteiro.

## Evitando Armadilhas do Otimizador

O maior erro em benchmarks é medir código que o compilador removeu silenciosamente. Se o resultado de uma computação não é usado, o otimizador em `-OReleaseFast` pode eliminar todo o loop. A solução é `std.mem.doNotOptimizeAway`:

```zig
const std = @import("std");

fn fibonacci(n: u32) u64 {
    if (n <= 1) return n;
    var a: u64 = 0;
    var b: u64 = 1;
    for (2..n + 1) |_| {
        const temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

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

    for (0..100_000) |_| {
        const result = fibonacci(30);
        std.mem.doNotOptimizeAway(result);
    }

    const elapsed = timer.read();
    std.debug.print("Média por iteração: {d:.1}ns\n", .{
        @as(f64, @floatFromInt(elapsed)) / 100_000.0,
    });
}
```

A função `doNotOptimizeAway` cria uma barreira de otimização sem custo real em runtime — ela apenas informa ao compilador que o valor é "utilizado", impedindo a eliminação de código morto.

## Estruturando Benchmarks Reprodutíveis

Para resultados confiáveis, um benchmark precisa de aquecimento, múltiplas iterações e análise estatística. Veja uma estrutura robusta:

```zig
const std = @import("std");

fn runBenchmark(comptime func: anytype, args: anytype, iterations: u32) struct { min: u64, max: u64, mean: u64 } {
    var min: u64 = std.math.maxInt(u64);
    var max: u64 = 0;
    var total: u64 = 0;

    // Aquecimento — 10% das iterações
    const warmup = iterations / 10;
    for (0..warmup) |_| {
        const r = @call(.auto, func, args);
        std.mem.doNotOptimizeAway(r);
    }

    // Medição real
    for (0..iterations) |_| {
        var timer = std.time.Timer.start() catch unreachable;
        const r = @call(.auto, func, args);
        std.mem.doNotOptimizeAway(r);
        const elapsed = timer.read();

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

    return .{
        .min = min,
        .max = max,
        .mean = total / iterations,
    };
}

fn ordenarArray() [1000]u32 {
    var arr: [1000]u32 = undefined;
    for (&arr, 0..) |*v, i| {
        v.* = @intCast(1000 - i);
    }
    std.mem.sort(u32, &arr, {}, std.sort.asc(u32));
    return arr;
}

pub fn main() !void {
    const result = runBenchmark(ordenarArray, .{}, 10_000);
    std.debug.print("Ordenação de 1000 elementos:\n", .{});
    std.debug.print("  Min:   {d:.1}us\n", .{@as(f64, @floatFromInt(result.min)) / 1000.0});
    std.debug.print("  Max:   {d:.1}us\n", .{@as(f64, @floatFromInt(result.max)) / 1000.0});
    std.debug.print("  Média: {d:.1}us\n", .{@as(f64, @floatFromInt(result.mean)) / 1000.0});
}
```

Essa abordagem com [comptime](/glossario/comptime/) genérico permite reusar a mesma estrutura para qualquer função que precise ser medida.

## Comparando Release Modes

Uma das vantagens do [build system](/artigos/zig-build-system-guia/) de Zig é a facilidade para alternar entre modos de compilação. Cada modo afeta diretamente a performance:

| Modo | Otimização | Safety Checks | Uso Típico |
|------|-----------|---------------|------------|
| Debug | Nenhuma | Todos | Desenvolvimento |
| ReleaseSafe | Máxima | Mantidos | Produção conservadora |
| ReleaseFast | Máxima | Removidos | Performance máxima |
| ReleaseSmall | Tamanho | Removidos | [Embarcados](/artigos/zig-embarcados-iot/) |

Para comparar os modos diretamente via [build.zig](/artigos/zig-package-manager-guia-build-zig-zon/):

```bash
zig build -Doptimize=Debug && ./zig-out/bin/benchmark
zig build -Doptimize=ReleaseSafe && ./zig-out/bin/benchmark
zig build -Doptimize=ReleaseFast && ./zig-out/bin/benchmark
zig build -Doptimize=ReleaseSmall && ./zig-out/bin/benchmark
```

A diferença entre Debug e ReleaseFast pode ser de 10x a 100x dependendo do tipo de cálculo. Operações intensivas em [SIMD](/artigos/zig-simd-processamento-vetorial/) se beneficiam enormemente da autovetorização habilitada em ReleaseFast.

## Integração com Tracy para Profiling Visual

Para ir além de microbenchmarks e identificar gargalos em aplicações completas, o [Tracy](/artigos/zig-depuracao-profiling-tracy-valgrind-perf/) é a ferramenta de escolha no ecossistema Zig. A integração é nativa via `std.debug`:

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

pub fn processarDados(dados: []const u8) !void {
    const zone = tracy.trace(@src());
    defer zone.end();

    // Código instrumentado — Tracy mede automaticamente
    var checksum: u64 = 0;
    for (dados) |byte| {
        checksum = checksum *% 31 +% byte;
    }
    std.mem.doNotOptimizeAway(checksum);
}
```

O profiler Tracy gera visualizações interativas com flamegraphs, histogramas de latência e uso de memória por [allocator](/glossario/allocator/). Isso complementa benchmarks pontuais com uma visão sistêmica do comportamento da aplicação.

## Benchmark Prático: Hashing

Vamos comparar duas implementações de hash para medir o impacto real de diferentes abordagens:

```zig
const std = @import("std");

fn hashSimples(data: []const u8) u64 {
    var h: u64 = 0;
    for (data) |byte| {
        h = h *% 31 +% byte;
    }
    return h;
}

fn hashFnv1a(data: []const u8) u64 {
    var h: u64 = 0xcbf29ce484222325;
    for (data) |byte| {
        h ^= byte;
        h *%= 0x100000001b3;
    }
    return h;
}

pub fn main() !void {
    const dados = "benchmark de hashing em zig para comparar implementações" ** 100;

    // Benchmark hash simples
    var timer1 = try std.time.Timer.start();
    for (0..100_000) |_| {
        std.mem.doNotOptimizeAway(hashSimples(dados));
    }
    const t1 = timer1.read();

    // Benchmark FNV-1a
    var timer2 = try std.time.Timer.start();
    for (0..100_000) |_| {
        std.mem.doNotOptimizeAway(hashFnv1a(dados));
    }
    const t2 = timer2.read();

    std.debug.print("Hash simples: {d:.2}ms\n", .{@as(f64, @floatFromInt(t1)) / 1_000_000.0});
    std.debug.print("FNV-1a:       {d:.2}ms\n", .{@as(f64, @floatFromInt(t2)) / 1_000_000.0});
    std.debug.print("Razão:        {d:.2}x\n", .{@as(f64, @floatFromInt(t1)) / @as(f64, @floatFromInt(t2))});
}
```

Esse padrão de comparação lado a lado é fundamental para avaliar trade-offs entre simplicidade e performance — algo recorrente quando se trabalha com [error handling](/artigos/zig-error-handling-boas-praticas/) e abstrações.

## Validando com hyperfine e perf stat

Depois que o microbenchmark em Zig parece estável, vale confirmar o resultado fora do próprio programa. Duas ferramentas simples ajudam muito: `hyperfine` para comparar comandos completos e `perf stat` para enxergar contadores do Linux. Essa camada externa reduz o risco de você medir apenas a lógica do temporizador ou uma execução excepcionalmente favorável.

Com `hyperfine`, compare binários compilados em modos diferentes ou versões alternativas do mesmo algoritmo:

```bash
zig build-exe benchmark.zig -OReleaseSafe -femit-bin=bench-safe
zig build-exe benchmark.zig -OReleaseFast -femit-bin=bench-fast

hyperfine --warmup 5 './bench-safe' './bench-fast'
```

Use `--warmup` para aquecer cache, carregamento de página e branch predictor. Quando o benchmark lê arquivos, rede local ou diretórios grandes, prefira preparar o ambiente antes e medir apenas o caminho que você quer comparar. Para resultados publicáveis, salve o JSON do `hyperfine` e mantenha a versão do compilador junto do relatório:

```bash
hyperfine --warmup 5 --export-json bench-zig.json './bench-fast'
zig version > zig-version.txt
```

No Linux, `perf stat` responde outra pergunta: não só quanto tempo demorou, mas por quê. Ele mostra ciclos, instruções, cache misses, branch misses e trocas de contexto.

```bash
perf stat -d ./bench-fast
```

Se uma otimização reduz tempo mas aumenta muito cache misses, talvez ela só funcione para o tamanho de entrada testado. Se o número de instruções cai mas o tempo não muda, o gargalo pode estar em memória, syscalls ou sincronização. Para investigar a função exata, avance para `perf record` e flamegraph no guia de [depuração e profiling com perf e Tracy](/artigos/zig-depuracao-profiling-tracy-valgrind-perf/).

Uma rotina prática para mudanças de performance em Zig fica assim:

1. escreva um microbenchmark com `std.time.Timer`;
2. impeça eliminação com `std.mem.doNotOptimizeAway`;
3. valide saída correta antes de medir;
4. compare binários com `hyperfine`;
5. confira contadores com `perf stat`;
6. só então declare a melhoria.

Esse fluxo é mais lento do que rodar um único comando, mas evita a armadilha clássica: otimizar um benchmark que não representa o programa real.

## Comparando com C e Outras Linguagens

Um benchmark relevante é comparar Zig com implementações equivalentes em [C](/artigos/zig-vs-c-moderno/). Graças à [interoperabilidade nativa](/artigos/zig-interoperabilidade-c/), você pode até chamar código C diretamente no mesmo benchmark:

```zig
const c = @cImport({
    @cInclude("string.h");
});

fn benchmarkMemcpy(dst: [*]u8, src: [*]const u8, len: usize) void {
    _ = c.memcpy(dst, src, len);
}
```

Essa capacidade de comparação direta é uma vantagem significativa do ecossistema Zig. Em nosso [comparativo entre Zig, Rust e Go](/artigos/zig-rust-go-comparativo/), os resultados de benchmarks em operações de baixo nível mostram Zig consistentemente próximo ao C puro.

## Boas Práticas para Benchmarks Confiáveis

Para garantir resultados que reflitam a realidade:

- **Desative frequency scaling**: use `cpupower frequency-set -g performance` no Linux
- **Feche outros processos**: isolamento reduz variância entre execuções
- **Use múltiplas iterações**: nunca confie em uma única medição
- **Reporte min, max e média**: outliers revelam problemas de medição
- **Teste com dados realistas**: benchmarks sintéticos podem otimizar caminhos irreais
- **Compare modos de compilação**: cada release mode tem perfil diferente
- **Verifique resultados**: sempre valide que o código medido produz saída correta

Se você vem de [Rust](https://rustlang.com.br/), já conhece a importância do benchmarking com Criterion. Em Zig, a filosofia é similar, mas sem dependências externas — as primitivas da <a href="https://rustlang.com.br/artigos/rust-benchmark-criterion/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">biblioteca padrão</a> são suficientes para a maioria dos cenários.

Para quem trabalha com <a href="https://golang.com.br/artigos/go-benchmark-testing/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">benchmarks em Go</a>, a diferença principal é que Zig não tem garbage collector, então não há pausas de GC contaminando as medições.

## Próximos Passos

Com benchmarks confiáveis em mãos, você pode otimizar aplicações reais. Considere explorar:

- [Processamento vetorial com SIMD](/artigos/zig-simd-processamento-vetorial/) para acelerar operações em arrays
- [Testes automatizados](/artigos/zig-testes-guia-completo/) para validar que otimizações não quebram funcionalidade
- [Criação de CLIs profissionais](/artigos/zig-cli-aplicacao-linha-comando/) para expor suas ferramentas de benchmark como utilitários de linha de comando
- [Cross-compilation](/artigos/zig-cross-compilation-guia/) para testar performance em diferentes arquiteturas
- [Concorrência avançada](/artigos/zig-concorrencia-padroes-avancados/) para benchmarks de throughput multithread
