Performance Lenta em Zig — Diagnosticar e Otimizar
Zig é projetado para alta performance, mas código lento pode acontecer por vários motivos. Este guia ajuda a identificar gargalos e otimizar seu código.
Passo 1: Verifique o Modo de Compilação
O erro mais comum: medir performance em modo Debug.
# ERRADO: Debug é 10-50x mais lento
zig build
./zig-out/bin/meu-app # Lento!
# CORRETO: Medir em Release
zig build -Doptimize=ReleaseFast
./zig-out/bin/meu-app # Rápido!
# Ou ReleaseSafe (para produção com segurança)
zig build -Doptimize=ReleaseSafe
Debug mode inclui bounds checking, overflow detection e nenhuma otimização. Sempre meça performance em Release.
Passo 2: Profiling
Usando perf (Linux)
# Compilar com símbolos de debug em release
zig build -Doptimize=ReleaseFast
# Profile com perf
perf record ./zig-out/bin/meu-app
perf report
# Flamegraph
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
Usando Instruments (macOS)
zig build -Doptimize=ReleaseFast
# Abrir Instruments.app e usar o template "Time Profiler"
Usando Timer do Zig
const std = @import("std");
pub fn main() !void {
var timer = try std.time.Timer.start();
// Código a medir
processar();
const ns = timer.read();
std.debug.print("Tempo: {d:.3}ms\n", .{
@as(f64, @floatFromInt(ns)) / 1_000_000.0,
});
}
Problema: Alocação Excessiva
Alocação de memória é cara. Cada alloc/free tem overhead.
// LENTO: aloca em cada iteração
fn processar_lento(allocator: std.mem.Allocator, dados: []const []const u8) !void {
for (dados) |item| {
const temp = try allocator.alloc(u8, item.len);
defer allocator.free(temp);
// ...processar...
}
}
// RÁPIDO: reutilizar buffer
fn processar_rapido(dados: []const []const u8) void {
var buffer: [4096]u8 = undefined;
for (dados) |item| {
if (item.len <= buffer.len) {
@memcpy(buffer[0..item.len], item);
// ...processar usando buffer...
}
}
}
// RÁPIDO: usar ArenaAllocator
fn processar_arena(allocator: std.mem.Allocator, dados: []const []const u8) !void {
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); // Uma única liberação
const temp_alloc = arena.allocator();
for (dados) |item| {
const temp = try temp_alloc.alloc(u8, item.len);
_ = temp;
// Sem free individual necessário
}
}
Problema: Cache Misses
Acesso não sequencial a memória causa cache misses, que são muito lentos.
// LENTO: acesso aleatório (cache miss)
fn soma_aleatoria(dados: []const i32, indices: []const usize) i64 {
var soma: i64 = 0;
for (indices) |i| {
soma += dados[i]; // Acesso não sequencial
}
return soma;
}
// RÁPIDO: acesso sequencial (cache friendly)
fn soma_sequencial(dados: []const i32) i64 {
var soma: i64 = 0;
for (dados) |v| {
soma += v; // Acesso sequencial, prefetch funciona
}
return soma;
}
Dica: Prefira arrays contíguos (SoA - Struct of Arrays) a arrays de structs (AoS) quando performance importa:
// AoS (Array of Structs) — pior para cache quando acessa só um campo
const Particula_AoS = struct { x: f32, y: f32, z: f32, massa: f32 };
var particulas: [1000]Particula_AoS = undefined;
// SoA (Struct of Arrays) — melhor para cache
const Particulas_SoA = struct {
x: [1000]f32,
y: [1000]f32,
z: [1000]f32,
massa: [1000]f32,
};
Problema: Cópias Desnecessárias
// LENTO: struct grande copiada a cada chamada
const DadosGrandes = struct { buffer: [65536]u8, meta: [1024]u8 };
fn processar_lento(dados: DadosGrandes) void { // Cópia de 66KB!
_ = dados;
}
// RÁPIDO: passar por ponteiro
fn processar_rapido(dados: *const DadosGrandes) void { // 8 bytes (ponteiro)
_ = dados;
}
Problema: Algoritmo Ineficiente
// LENTO: O(n^2) busca linear repetida
fn contem_duplicatas_lento(dados: []const i32) bool {
for (dados, 0..) |a, i| {
for (dados[i + 1 ..]) |b| {
if (a == b) return true;
}
}
return false;
}
// RÁPIDO: O(n) usando HashMap
fn contem_duplicatas_rapido(allocator: std.mem.Allocator, dados: []const i32) !bool {
var visto = std.AutoHashMap(i32, void).init(allocator);
defer visto.deinit();
for (dados) |v| {
if (visto.contains(v)) return true;
try visto.put(v, {});
}
return false;
}
Usando SIMD para Performance
// Soma SIMD — processa 8 elementos por vez
fn soma_simd(dados: []const f32) f32 {
const Vec8 = @Vector(8, f32);
var acumulador: Vec8 = @splat(0.0);
var i: usize = 0;
while (i + 8 <= dados.len) : (i += 8) {
const chunk: Vec8 = dados[i..][0..8].*;
acumulador += chunk;
}
var total: f32 = @reduce(.Add, acumulador);
// Processar elementos restantes
while (i < dados.len) : (i += 1) {
total += dados[i];
}
return total;
}
Otimizações com comptime
// Pré-calcular em compilação
const lookup_table = comptime blk: {
var table: [256]u8 = undefined;
for (0..256) |i| {
table[i] = @popCount(@as(u8, @intCast(i)));
}
break :blk table;
};
// Zero custo em runtime
fn popcount_rapido(byte: u8) u8 {
return lookup_table[byte];
}
Checklist de Performance
- Compilar em
ReleaseFastouReleaseSafepara medir - Profile antes de otimizar — encontre o gargalo real
- Reduzir alocações dinâmicas (reutilizar buffers, usar arena)
- Melhorar localidade de cache (acesso sequencial, SoA)
- Evitar cópias de structs grandes (passar por ponteiro)
- Usar algoritmos adequados (complexidade correta)
- Considerar SIMD para processamento numérico
- Usar
comptimepara pré-calcular valores
Veja Também
- FAQ Performance — Conceitos de performance
- Compilação Lenta — Performance de compilação
- FAQ Memória — Otimização de memória
- Memory Leak — Leaks afetam performance
- Receitas — Exemplos otimizados