Performance Lenta em Zig — Diagnosticar e Otimizar

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

  1. Compilar em ReleaseFast ou ReleaseSafe para medir
  2. Profile antes de otimizar — encontre o gargalo real
  3. Reduzir alocações dinâmicas (reutilizar buffers, usar arena)
  4. Melhorar localidade de cache (acesso sequencial, SoA)
  5. Evitar cópias de structs grandes (passar por ponteiro)
  6. Usar algoritmos adequados (complexidade correta)
  7. Considerar SIMD para processamento numérico
  8. Usar comptime para pré-calcular valores

Veja Também

Continue aprendendo Zig

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