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
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento básico de Zig. Consulte a introdução ao Zig
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
- Compile com ReleaseFast para benchmarks:
zig build -Doptimize=ReleaseFast - Faça warmup: Ignore as primeiras iterações
- Use
doNotOptimizeAway: Previna eliminação de código pelo compilador - Meça múltiplas vezes: Use min/max/média para resultado confiável
- Isole o código medido: Evite medir setup/teardown
- Compare com baseline: Sempre compare com uma referência
- 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.