Teoria sem pratica nao otimiza nada. Neste artigo final da serie, vamos otimizar um projeto real passo a passo — um processador de logs que analisa milhoes de linhas — aplicando todas as tecnicas que aprendemos: benchmarking, cache-friendly layout, SIMD e profiling. Cada etapa inclui medicoes antes e depois.
Continuacao do artigo sobre ferramentas de profiling.
O Projeto: Analisador de Logs
Nosso projeto e um analisador de logs de acesso HTTP que precisa:
- Ler linhas de log do formato:
IP METODO URL STATUS TEMPO_MS - Contar requisicoes por status code (200, 404, 500, etc.)
- Calcular tempo medio de resposta
- Encontrar os IPs com mais requisicoes
Meta: processar 10 milhoes de linhas o mais rapido possivel.
Versao Inicial: Ingênua
const std = @import("std");
const LogEntry = struct {
ip: []const u8,
metodo: []const u8,
url: []const u8,
status: u16,
tempo_ms: f64,
};
const Estatisticas = struct {
total_requests: u64 = 0,
soma_tempo: f64 = 0,
contagem_status: std.AutoHashMap(u16, u64),
contagem_ips: std.StringHashMap(u64),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Estatisticas {
return .{
.contagem_status = std.AutoHashMap(u16, u64).init(allocator),
.contagem_ips = std.StringHashMap(u64).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Estatisticas) void {
self.contagem_status.deinit();
// Liberar chaves alocadas
var it = self.contagem_ips.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
}
self.contagem_ips.deinit();
}
pub fn registrar(self: *Estatisticas, entry: LogEntry) !void {
self.total_requests += 1;
self.soma_tempo += entry.tempo_ms;
// Contagem por status
const status_count = try self.contagem_status.getOrPut(entry.status);
if (!status_count.found_existing) status_count.value_ptr.* = 0;
status_count.value_ptr.* += 1;
// Contagem por IP
const ip_count = try self.contagem_ips.getOrPut(
try self.allocator.dupe(u8, entry.ip),
);
if (!ip_count.found_existing) ip_count.value_ptr.* = 0;
ip_count.value_ptr.* += 1;
}
pub fn tempoMedio(self: Estatisticas) f64 {
if (self.total_requests == 0) return 0;
return self.soma_tempo / @as(f64, @floatFromInt(self.total_requests));
}
};
/// Versao V1: implementacao ingenua
fn parseLine(linha: []const u8) !LogEntry {
var partes = std.mem.splitScalar(u8, linha, ' ');
const ip = partes.next() orelse return error.FormatoInvalido;
const metodo = partes.next() orelse return error.FormatoInvalido;
const url = partes.next() orelse return error.FormatoInvalido;
const status_str = partes.next() orelse return error.FormatoInvalido;
const tempo_str = partes.next() orelse return error.FormatoInvalido;
return .{
.ip = ip,
.metodo = metodo,
.url = url,
.status = try std.fmt.parseInt(u16, status_str, 10),
.tempo_ms = try std.fmt.parseFloat(f64, tempo_str),
};
}
fn analisarV1(dados: []const u8, allocator: std.mem.Allocator) !Estatisticas {
var stats = Estatisticas.init(allocator);
var linhas = std.mem.splitScalar(u8, dados, '\n');
while (linhas.next()) |linha| {
if (linha.len == 0) continue;
const entry = parseLine(linha) catch continue;
try stats.registrar(entry);
}
return stats;
}
Benchmark V1
V1 (Ingênua): 10M linhas em 4.200 ms
- Parse: 1.800 ms
- Estatísticas: 2.400 ms
- Memória: 890 MB
Otimizacao 1: Reduzir Alocacoes
O maior gargalo e a alocacao de strings para cada IP. Vamos usar um string intern pool:
const StringPool = struct {
strings: std.StringHashMap(void),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) StringPool {
return .{
.strings = std.StringHashMap(void).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *StringPool) void {
var it = self.strings.keyIterator();
while (it.next()) |key| {
self.allocator.free(key.*);
}
self.strings.deinit();
}
/// Retorna ponteiro estavel para string interned
pub fn intern(self: *StringPool, str: []const u8) ![]const u8 {
if (self.strings.getKey(str)) |existing| {
return existing; // Ja existe, reutilizar
}
const owned = try self.allocator.dupe(u8, str);
try self.strings.put(owned, {});
return owned;
}
};
/// V2: com string interning
fn analisarV2(dados: []const u8, allocator: std.mem.Allocator) !Estatisticas {
var pool = StringPool.init(allocator);
defer pool.deinit();
var stats = Estatisticas.init(allocator);
var linhas = std.mem.splitScalar(u8, dados, '\n');
while (linhas.next()) |linha| {
if (linha.len == 0) continue;
var entry = parseLine(linha) catch continue;
entry.ip = try pool.intern(entry.ip);
try stats.registrar(entry);
}
return stats;
}
V2 (String interning): 10M linhas em 2.800 ms (-33%)
- Memória: 320 MB (-64%)
Otimizacao 2: Parser Otimizado
Em vez de usar std.fmt.parseFloat, que e generico, escrevemos um parser especializado para nosso formato:
/// Parser rapido de inteiros — sem tratamento de erros generico
fn parseIntFast(str: []const u8) u16 {
var resultado: u16 = 0;
for (str) |c| {
resultado = resultado * 10 + @as(u16, c - '0');
}
return resultado;
}
/// Parser rapido de float simples (formato: DDDD.DD)
fn parseFloatFast(str: []const u8) f64 {
var parte_inteira: u64 = 0;
var parte_decimal: u64 = 0;
var casas_decimais: u32 = 0;
var no_decimal = false;
for (str) |c| {
if (c == '.') {
no_decimal = true;
continue;
}
if (no_decimal) {
parte_decimal = parte_decimal * 10 + (c - '0');
casas_decimais += 1;
} else {
parte_inteira = parte_inteira * 10 + (c - '0');
}
}
var divisor: f64 = 1.0;
for (0..casas_decimais) |_| divisor *= 10.0;
return @as(f64, @floatFromInt(parte_inteira)) +
@as(f64, @floatFromInt(parte_decimal)) / divisor;
}
/// V3: parser otimizado com busca manual de delimitadores
fn parseLineV3(linha: []const u8) !LogEntry {
// Buscar espacos manualmente — mais rapido que split iterator
var espacos: [4]usize = undefined;
var count: usize = 0;
for (linha, 0..) |c, i| {
if (c == ' ') {
if (count >= 4) break;
espacos[count] = i;
count += 1;
}
}
if (count < 4) return error.FormatoInvalido;
return .{
.ip = linha[0..espacos[0]],
.metodo = linha[espacos[0] + 1 .. espacos[1]],
.url = linha[espacos[1] + 1 .. espacos[2]],
.status = parseIntFast(linha[espacos[2] + 1 .. espacos[3]]),
.tempo_ms = parseFloatFast(linha[espacos[3] + 1 ..]),
};
}
V3 (Parser otimizado): 10M linhas em 1.600 ms (-43%)
- Parse: 450 ms (-75% vs V1)
Otimizacao 3: Contagem com Array Fixo
Para status codes (que sao um conjunto pequeno e previsivel), substituir HashMap por array direto:
const EstatisticasV4 = struct {
total_requests: u64 = 0,
soma_tempo: f64 = 0,
// Array direto indexado por status code (100-599)
contagem_status: [600]u64 = [_]u64{0} ** 600,
contagem_ips: std.StringHashMap(u64),
allocator: std.mem.Allocator,
pub fn registrar(self: *EstatisticasV4, entry: LogEntry) !void {
self.total_requests += 1;
self.soma_tempo += entry.tempo_ms;
// Acesso direto — O(1), sem hash, sem colisao
self.contagem_status[entry.status] += 1;
// IPs ainda usam hashmap
const result = try self.contagem_ips.getOrPut(entry.ip);
if (!result.found_existing) result.value_ptr.* = 0;
result.value_ptr.* += 1;
}
};
V4 (Array para status): 10M linhas em 1.200 ms (-25%)
Otimizacao 4: Processamento SIMD para Busca de Newlines
Encontrar quebras de linha com SIMD:
/// Encontrar proxima newline usando SIMD
fn encontrarNewline(dados: []const u8) ?usize {
const vec_len = 32;
const newline_vec: @Vector(vec_len, u8) = @splat('\n');
var i: usize = 0;
while (i + vec_len <= dados.len) : (i += vec_len) {
const bloco: @Vector(vec_len, u8) = dados[i..][0..vec_len].*;
const mask = bloco == newline_vec;
// Encontrar primeiro bit setado
const bits: u32 = @bitCast(mask);
if (bits != 0) {
return i + @ctz(bits);
}
}
// Tail loop
while (i < dados.len) : (i += 1) {
if (dados[i] == '\n') return i;
}
return null;
}
/// V5: split de linhas com SIMD
fn analisarV5(dados: []const u8, allocator: std.mem.Allocator) !EstatisticasV4 {
var stats = EstatisticasV4{
.contagem_ips = std.StringHashMap(u64).init(allocator),
.allocator = allocator,
};
var pos: usize = 0;
while (pos < dados.len) {
const restante = dados[pos..];
const fim_linha = encontrarNewline(restante) orelse restante.len;
if (fim_linha > 0) {
const entry = parseLineV3(restante[0..fim_linha]) catch {
pos += fim_linha + 1;
continue;
};
try stats.registrar(entry);
}
pos += fim_linha + 1;
}
return stats;
}
V5 (SIMD newline scan): 10M linhas em 850 ms (-29%)
Resultados Consolidados
| Versao | Tempo | Speedup | Memoria | Tecnica |
|---|---|---|---|---|
| V1 | 4.200 ms | 1.0x | 890 MB | Ingênua |
| V2 | 2.800 ms | 1.5x | 320 MB | String interning |
| V3 | 1.600 ms | 2.6x | 320 MB | Parser otimizado |
| V4 | 1.200 ms | 3.5x | 315 MB | Array para status |
| V5 | 850 ms | 4.9x | 315 MB | SIMD newline scan |
Resultado final: 4.9x mais rapido, 65% menos memoria.
Quando Parar de Otimizar
Sinais de que Voce Deve Parar
- O codigo esta rapido o suficiente para os requisitos
- O gargalo mudou para I/O, rede ou banco de dados
- A legibilidade esta comprometida demais
- Os ganhos sao marginais (otimizar de 850ms para 820ms raramente justifica)
Trade-offs
// Legivel mas lento (V1):
var partes = std.mem.splitScalar(u8, linha, ' ');
const ip = partes.next() orelse return error.FormatoInvalido;
// Rapido mas menos legivel (V3):
var espacos: [4]usize = undefined;
var count: usize = 0;
for (linha, 0..) |c, i| {
if (c == ' ') { espacos[count] = i; count += 1; if (count >= 4) break; }
}
A decisao de qual versao usar depende do contexto:
- Script de uso ocasional: V1 e perfeito
- Servico de producao com alto volume: V5 justifica a complexidade
- Biblioteca publica: V3 oferece bom equilibrio
Checklist de Otimizacao
- Medir — Profile primeiro, identifique o hotspot real
- Algoritmo — A complexidade algoritmica e correta?
- Alocacoes — Pode reutilizar memoria em vez de alocar?
- Layout — Os dados estao cache-friendly (SoA)?
- Branches — Pode eliminar condicionais do hot path?
- SIMD — O loop processa dados independentes?
- Medir de novo — A otimizacao realmente melhorou?
Conclusao
Otimizacao de performance e um processo iterativo e mensuravel. Nesta serie, aprendemos a medir corretamente com benchmarking, organizar dados para maximizar cache hits, usar SIMD para processamento paralelo, e encontrar gargalos com ferramentas de profiling. O estudo de caso demonstrou que melhorias de 5x sao alcancaveis com tecnicas sistematicas em Zig.
Serie Completa
Esta e a conclusao da serie Otimizacao de Performance em Zig:
- Tecnicas de Benchmarking
- Codigo Cache-Friendly
- SIMD e Vetorizacao
- Ferramentas de Profiling
- Otimizacao Real: Estudo de Caso (este artigo)
Conteudo Relacionado
- Zig em Fintech e Trading — Performance em producao
- Zig em Telecomunicacoes — Alta performance em telecom
- Masterclass Memoria — Gerenciamento avancado
- Clean Code em Zig — Equilibrio legibilidade-performance
- Estrategias de Alocacao — Memoria avancada