“Se voce nao mede, voce nao sabe.” Essa maxima e especialmente verdadeira em otimizacao de performance. Antes de otimizar qualquer coisa, voce precisa saber medir corretamente. Neste artigo, exploramos tecnicas de benchmarking em Zig que produzem resultados confiaveis e reproduziveis.
Primeiro artigo da serie Otimizacao de Performance em Zig.
Por Que Benchmarking e Dificil
Medir performance de forma precisa e surpreendentemente complicado:
- Cache do processador muda resultados entre execucoes
- Branch prediction favorece caminhos executados repetidamente
- Frequency scaling da CPU altera a velocidade durante a execucao
- Otimizacoes do compilador podem eliminar codigo “morto”
- Outros processos competem por recursos
Timer de Alta Resolucao
std.time.Timer
O Zig oferece um timer de alta resolucao para benchmarking:
const std = @import("std");
pub fn benchmark(comptime func: fn () void) struct { min: u64, max: u64, media: u64 } {
const N = 1000;
var tempos: [N]u64 = undefined;
for (&tempos) |*t| {
var timer = std.time.Timer.start() catch unreachable;
func();
t.* = timer.read(); // Nanosegundos
}
// Ordenar para calcular percentis
std.mem.sort(u64, &tempos, {}, std.sort.asc(u64));
var soma: u64 = 0;
for (tempos) |t| soma += t;
return .{
.min = tempos[0],
.max = tempos[N - 1],
.media = soma / N,
};
}
test "benchmark soma de array" {
const dados = blk: {
var arr: [10_000]u32 = undefined;
for (&arr, 0..) |*v, i| v.* = @intCast(i);
break :blk arr;
};
const resultado = benchmark(struct {
fn run() void {
var soma: u64 = 0;
for (&dados) |v| soma += v;
std.mem.doNotOptimizeAway(soma);
}
}.run);
std.debug.print(
\\Benchmark soma_array:
\\ Min: {d} ns
\\ Media: {d} ns
\\ Max: {d} ns
\\
, .{ resultado.min, resultado.media, resultado.max });
}
Evitando que o Compilador Elimine seu Codigo
O maior erro em benchmarking e o compilador otimizar o codigo que voce quer medir:
const std = @import("std");
fn somaIngenua(dados: []const u32) u64 {
var soma: u64 = 0;
for (dados) |v| soma += v;
return soma;
}
test "benchmark ERRADO — compilador pode eliminar" {
const dados = [_]u32{ 1, 2, 3, 4, 5 } ** 2000;
var timer = try std.time.Timer.start();
// PROBLEMA: resultado nao e usado, compilador pode eliminar tudo
_ = somaIngenua(&dados);
const elapsed = timer.read();
std.debug.print("Tempo: {d} ns\n", .{elapsed});
}
test "benchmark CORRETO — previne eliminacao" {
const dados = [_]u32{ 1, 2, 3, 4, 5 } ** 2000;
var timer = try std.time.Timer.start();
const resultado = somaIngenua(&dados);
// doNotOptimizeAway previne que o compilador elimine o calculo
std.mem.doNotOptimizeAway(resultado);
const elapsed = timer.read();
std.debug.print("Tempo: {d} ns, resultado: {d}\n", .{ elapsed, resultado });
}
Framework de Benchmarking Robusto
Benchmark com Warmup e Estatisticas
const std = @import("std");
const BenchmarkConfig = struct {
warmup_iterations: u32 = 100,
iterations: u32 = 10_000,
nome: []const u8 = "benchmark",
};
const BenchmarkResult = struct {
nome: []const u8,
min_ns: u64,
max_ns: u64,
media_ns: u64,
mediana_ns: u64,
p95_ns: u64,
p99_ns: u64,
throughput_ops_s: f64,
pub fn imprimir(self: BenchmarkResult) void {
std.debug.print(
\\=== {s} ===
\\ Min: {d:>10} ns
\\ Mediana: {d:>10} ns
\\ Media: {d:>10} ns
\\ P95: {d:>10} ns
\\ P99: {d:>10} ns
\\ Max: {d:>10} ns
\\ Throughput: {d:.2} ops/s
\\
, .{
self.nome,
self.min_ns,
self.mediana_ns,
self.media_ns,
self.p95_ns,
self.p99_ns,
self.max_ns,
self.throughput_ops_s,
});
}
};
fn runBenchmark(config: BenchmarkConfig, comptime func: fn () void) BenchmarkResult {
// Warmup: aquecer caches e branch predictor
for (0..config.warmup_iterations) |_| {
func();
}
// Medicao real
const allocator = std.heap.page_allocator;
const tempos = allocator.alloc(u64, config.iterations) catch unreachable;
defer allocator.free(tempos);
for (tempos) |*t| {
var timer = std.time.Timer.start() catch unreachable;
func();
t.* = timer.read();
}
// Ordenar para percentis
std.mem.sort(u64, tempos, {}, std.sort.asc(u64));
var soma: u128 = 0;
for (tempos) |t| soma += t;
const media = @as(u64, @intCast(soma / config.iterations));
return .{
.nome = config.nome,
.min_ns = tempos[0],
.max_ns = tempos[config.iterations - 1],
.media_ns = media,
.mediana_ns = tempos[config.iterations / 2],
.p95_ns = tempos[@as(usize, @intFromFloat(@as(f64, @floatFromInt(config.iterations)) * 0.95))],
.p99_ns = tempos[@as(usize, @intFromFloat(@as(f64, @floatFromInt(config.iterations)) * 0.99))],
.throughput_ops_s = if (media > 0) 1_000_000_000.0 / @as(f64, @floatFromInt(media)) else 0,
};
}
Comparando Implementacoes
A/B Testing de Algoritmos
const std = @import("std");
/// Busca linear — O(n)
fn buscaLinear(dados: []const u32, alvo: u32) ?usize {
for (dados, 0..) |v, i| {
if (v == alvo) return i;
}
return null;
}
/// Busca binaria — O(log n)
fn buscaBinaria(dados: []const u32, alvo: u32) ?usize {
var low: usize = 0;
var high: usize = dados.len;
while (low < high) {
const mid = low + (high - low) / 2;
if (dados[mid] == alvo) return mid;
if (dados[mid] < alvo) {
low = mid + 1;
} else {
high = mid;
}
}
return null;
}
test "comparar busca linear vs binaria" {
// Dados ordenados
const N = 100_000;
var dados: [N]u32 = undefined;
for (&dados, 0..) |*v, i| v.* = @intCast(i * 2);
const alvo: u32 = N; // Elemento no meio
const resultado_linear = runBenchmark(.{
.nome = "Busca Linear",
.iterations = 1000,
}, struct {
fn run() void {
const r = buscaLinear(&dados, alvo);
std.mem.doNotOptimizeAway(r);
}
}.run);
const resultado_binaria = runBenchmark(.{
.nome = "Busca Binaria",
.iterations = 1000,
}, struct {
fn run() void {
const r = buscaBinaria(&dados, alvo);
std.mem.doNotOptimizeAway(r);
}
}.run);
resultado_linear.imprimir();
resultado_binaria.imprimir();
// Busca binaria deve ser significativamente mais rapida
std.debug.print("Speedup: {d:.1}x\n", .{
@as(f64, @floatFromInt(resultado_linear.media_ns)) /
@as(f64, @floatFromInt(resultado_binaria.media_ns)),
});
}
Modos de Otimizacao do Zig
Comparando Debug, ReleaseSafe, ReleaseFast e ReleaseSmall
const std = @import("std");
/// Funcao computacionalmente intensiva
fn fibonacci(n: u64) u64 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
test "benchmark fibonacci nos diferentes modos" {
// Este teste deve ser executado com cada modo:
// zig build test -Doptimize=Debug
// zig build test -Doptimize=ReleaseSafe
// zig build test -Doptimize=ReleaseFast
// zig build test -Doptimize=ReleaseSmall
const modo = if (@import("builtin").mode == .Debug)
"Debug"
else if (@import("builtin").mode == .ReleaseSafe)
"ReleaseSafe"
else if (@import("builtin").mode == .ReleaseFast)
"ReleaseFast"
else
"ReleaseSmall";
var timer = try std.time.Timer.start();
const resultado = fibonacci(35);
const elapsed = timer.read();
std.mem.doNotOptimizeAway(resultado);
std.debug.print("[{s}] fibonacci(35) = {d} em {d} ms\n", .{
modo,
resultado,
elapsed / std.time.ns_per_ms,
});
}
Resultados tipicos (Apple M2):
| Modo | fibonacci(35) | Tempo |
|---|---|---|
| Debug | 9227465 | ~850 ms |
| ReleaseSafe | 9227465 | ~45 ms |
| ReleaseFast | 9227465 | ~35 ms |
| ReleaseSmall | 9227465 | ~48 ms |
Armadilhas Comuns
1. Nao Aquecer Caches
// ERRADO: primeira iteracao sera lenta (cold cache)
var timer = try std.time.Timer.start();
for (0..100) |_| func();
const total = timer.read();
// CORRETO: warmup antes de medir
for (0..100) |_| func(); // Warmup
var timer = try std.time.Timer.start();
for (0..100) |_| func(); // Medicao
const total = timer.read();
2. Medir Apenas a Media
// ERRADO: media esconde outliers
const media = total_ns / iteracoes;
// CORRETO: reportar percentis
// Min, P50 (mediana), P95, P99, Max
// P99 e o que realmente importa em producao
3. Benchmarking com Dados Irrealistas
// ERRADO: dados sequenciais sempre cabem no cache
const dados = [_]u32{1, 2, 3, 4, 5} ** 1000;
// CORRETO: dados aleatorios simulam cenario real
var rng = std.Random.DefaultPrng.init(42);
var dados: [5000]u32 = undefined;
for (&dados) |*v| v.* = rng.random().int(u32);
Conclusao
Benchmarking correto e o fundamento de qualquer esforço de otimizacao. Sem medicoes confiaveis, voce pode acabar otimizando codigo que nao importa ou piorando performance sem perceber. Use as tecnicas deste artigo para estabelecer uma baseline solida antes de aplicar as otimizacoes que veremos nos proximos artigos.
Proximo Artigo
No Artigo 2: Codigo Cache-Friendly, exploramos como o layout de dados em memoria afeta dramaticamente a performance.
Conteudo Relacionado
- Codigo Cache-Friendly em Zig — Proximo artigo
- Profiling e Benchmarks em Zig — Tutorial basico
- Otimizacao Real: Estudo de Caso — Caso pratico
- Zig em Fintech e Trading — Performance em producao