SIMD e desempenho
Esta é a área em que Zig se destaca e o que atrai quem vem de motores de consulta, bancos colunares e código numérico. Não porque Zig tenha um truque secreto de velocidade, mas porque ele remove as camadas que ficam entre o seu código e o processador, e devolve a você o controle de layout, alinhamento e instruções vetoriais que uma linguagem com runtime pesado esconde. Desempenho, aqui, é menos sobre otimização esperta e mais sobre não pagar por abstrações que você não pediu.
@Vector: uma pista de dados vira muitas
Section titled “@Vector: uma pista de dados vira muitas”SIMD, “uma instrução, muitos dados”, é a capacidade do processador de aplicar a mesma operação a um punhado de valores de uma vez, num único ciclo. Zig expõe isso com o tipo @Vector, uma abstração portátil que o compilador mapeia para as instruções SIMD do alvo, SSE e AVX no x86, NEON no ARM, e para código escalar comum onde não houver suporte.
const V = @Vector(4, f32);
pub fn main() void { const a: V = .{ 1, 2, 3, 4 }; const b: V = .{ 10, 20, 30, 40 }; const soma = a + b; // uma instrução, quatro somas const total = @reduce(.Add, soma); // 4+... reduzido a um escalar std.debug.print("{d}\n", .{total});}O que impressiona é que os operadores comuns funcionam: a + b soma as quatro pistas ao mesmo tempo, e não é um laço disfarçado, é uma instrução de máquina. Em volta dos operadores há um punhado de builtins que formam o vocabulário de SIMD em Zig:
| Builtin | O que faz |
|---|---|
a + b, a * b, … | operam em todas as pistas de uma vez |
@splat(x) | transforma um escalar num vetor com todas as pistas iguais a x |
@reduce(.Add, v) | colapsa o vetor num escalar por uma operação associativa |
@shuffle(...) | reorganiza as pistas de um ou dois vetores numa ordem nova |
@select(pred, a, b) | escolhe pista a pista entre dois vetores, sem if |
É esse vocabulário que permite processar uma coluna inteira de um banco quatro, oito ou dezesseis valores por vez, e é a razão de motores como DuckDB extraírem tanto de tão pouco.
Layout e alinhamento decidem o ganho
Section titled “Layout e alinhamento decidem o ganho”O ganho de SIMD não é automático, e é aqui que os assuntos anteriores da trilha voltam a cobrar. As instruções vetoriais esperam dados contíguos e frequentemente alinhados, o alinhamento que a linguagem deixa você controlar. Se os valores que você quer somar estão espalhados pela memória, dentro de structs entremeados com outros campos, o processador gasta mais tempo juntando-os do que somando-os. É a diferença entre guardar os dados como um array de structs e como um struct de arrays: para percorrer uma coluna, o segundo layout é o que permite o SIMD render. Zig não escolhe o layout por você, e essa é justamente a alavanca.
Cache e desvios: o que faz rápido ser rápido
Section titled “Cache e desvios: o que faz rápido ser rápido”Boa parte do desempenho moderno não é sobre fazer menos contas, é sobre alimentar o processador sem que ele espere. A hierarquia de cache, o custo de um acesso à memória principal e o preço de uma previsão de desvio errada estão descritos no nível de sistema, e valem a leitura porque governam o que otimizar. Dados contíguos e percorridos em ordem mantêm a cache quente; saltos aleatórios pela memória a esfriam e param o processador. Um desvio condicional que o preditor erra descarta trabalho já adiantado, e é por isso que código sem ramos, usando @select para escolher sem if, às vezes vence código “mais inteligente” cheio de condicionais. A regra prática é uma só: meça, não adivinhe. O std.time.Timer e um benchmark honesto revelam gargalos que a intuição erra com frequência desconcertante, porque o custo real mora na cache e no preditor, não na contagem de operações.
Concorrência: threads, atômicos e o novo I/O
Section titled “Concorrência: threads, atômicos e o novo I/O”A concorrência de baixo nível em Zig é enxuta e explícita, coerente com o resto. std.Thread.spawn cria uma thread do sistema operacional, std.Thread.Mutex protege memória compartilhada, e std.atomic.Value(T) mais os builtins como @atomicRmw dão as operações atômicas com as quais se constrói coordenação sem lock. A teoria por trás disso, o que é uma race condition, por que um mutex resolve e o que um atômico garante, é o assunto da trilha de concorrência, que também posiciona Zig no mapa dos modelos; não vale reescrevê-la aqui.
O que vale registrar é o estado do modelo assíncrono, porque ele mudou de forma instrutiva. Zig teve async e await como palavras-chave, e os removeu: a conclusão foi que amarrar a linguagem a corrotinas sem pilha não servia à gama de alvos que Zig quer atender, do microcontrolador ao servidor de milhões de conexões. O rumo atual, que aparece parcialmente na linha 0.15 e ganha corpo na 0.16, é tratar I/O como se trata alocação. Assim como uma função recebe um Allocator, ela passa a receber um std.Io, e é esse objeto que decide se a espera é bloqueante, num pool de threads, ou em corrotinas. O efeito é dissolver o problema das “funções coloridas”: o mesmo código roda síncrono ou assíncrono conforme o Io que você entrega, sem duas versões de cada função. É o princípio de “não esconder nada” levado à espera: a I/O deixa de ser um poder ambiente e vira um parâmetro, exatamente como a memória.
Palavras-chave
Section titled “Palavras-chave”Conceitos: SIMD, vetorização, alinhamento, layout de memória, array de structs vs struct de arrays, cache, previsão de desvio, código sem ramos, medir vs adivinhar
Linguagem: @Vector, @reduce, @splat, @shuffle, @select, std.time.Timer
Concorrência: std.Thread, std.Thread.Mutex, std.atomic.Value, @atomicRmw, std.Io, funções coloridas
Hardware: SSE, AVX, NEON, hierarquia de cache, preditor de desvio