Vector SIMD em Zig — O que é e Como Usar
Definição
Vetores SIMD (Single Instruction, Multiple Data) em Zig são tipos que permitem executar a mesma operação em múltiplos dados simultaneamente, aproveitando instruções vetoriais da CPU como SSE, AVX (x86) ou NEON (ARM). O tipo @Vector(N, T) cria um vetor de N elementos do tipo T, e operações sobre ele são mapeadas diretamente para instruções SIMD do hardware.
Diferente de bibliotecas SIMD externas, vetores SIMD são cidadãos de primeira classe em Zig — suportam operadores aritméticos, comparações e indexação nativamente.
Por que Vetores SIMD Importam
- Performance: Processar 4, 8, 16 ou mais elementos em uma única instrução de CPU.
- Portabilidade: O compilador traduz para as instruções SIMD disponíveis na arquitetura-alvo.
- Sem assembly: Acesso a SIMD com a mesma sintaxe de Zig normal.
- Auto-vetorização: O LLVM backend pode otimizar ainda mais o código vetorial gerado.
Exemplo Prático
Operações Básicas com Vetores
const std = @import("std");
pub fn main() void {
const a: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const b: @Vector(4, f32) = .{ 5.0, 6.0, 7.0, 8.0 };
// Operações elemento a elemento
const soma = a + b; // { 6.0, 8.0, 10.0, 12.0 }
const produto = a * b; // { 5.0, 12.0, 21.0, 32.0 }
// Escalar broadcast
const dobro = a * @as(@Vector(4, f32), @splat(2.0));
std.debug.print("Soma: {d}\n", .{soma});
std.debug.print("Produto: {d}\n", .{produto});
std.debug.print("Dobro: {d}\n", .{dobro});
}
Soma de Array com SIMD
const std = @import("std");
fn somaSimd(dados: []const f32) f32 {
const VEC_LEN = 4;
var acumulador: @Vector(VEC_LEN, f32) = @splat(0.0);
var i: usize = 0;
while (i + VEC_LEN <= dados.len) : (i += VEC_LEN) {
const chunk: @Vector(VEC_LEN, f32) = dados[i..][0..VEC_LEN].*;
acumulador += chunk;
}
// Reduzir vetor para escalar
var total: f32 = @reduce(.Add, acumulador);
// Processar elementos restantes
while (i < dados.len) : (i += 1) {
total += dados[i];
}
return total;
}
pub fn main() void {
const dados = [_]f32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
const resultado = somaSimd(&dados);
std.debug.print("Soma SIMD: {d}\n", .{resultado}); // 55.0
}
Comparação Vetorial
const std = @import("std");
pub fn main() void {
const a: @Vector(4, i32) = .{ 10, 20, 30, 40 };
const b: @Vector(4, i32) = .{ 15, 15, 35, 35 };
// Comparação retorna vetor de bool
const maior = a > b; // { false, true, false, true }
// Selecionar baseado na comparação
const resultado = @select(i32, maior, a, b);
// { 15, 20, 35, 40 } — o maior de cada par
std.debug.print("Resultado: {}\n", .{resultado});
}
Operações Disponíveis
| Operação | Descrição |
|---|---|
+, -, *, / | Aritmética elemento a elemento |
>, <, ==, != | Comparação (retorna vetor de bool) |
@reduce(.Add, v) | Reduz vetor a escalar com soma |
@splat(valor) | Cria vetor com todos elementos iguais |
@select(T, mask, a, b) | Seleciona elementos por máscara |
@shuffle | Reorganiza elementos entre vetores |
Casos de Uso Práticos
Vetores SIMD brilham em operações que processam grandes volumes de dados numéricos de forma uniforme:
- Processamento de áudio/vídeo: Aplicar ganho, mixagem ou filtros em buffers de amostras.
- Computação gráfica: Transformações de vértices, produto escalar de vetores 3D.
- Machine learning: Operações de produto de matrizes (GEMM), normalização de camadas.
- Criptografia: Operações XOR em bloco, expansão de chave AES.
- Compressão: Busca de padrões em dados brutos.
const std = @import("std");
// Produto escalar de dois arrays usando SIMD
fn produtoEscalarSimd(a: []const f32, b: []const f32) f32 {
std.debug.assert(a.len == b.len);
const N = 4;
var acum: @Vector(N, f32) = @splat(0.0);
var i: usize = 0;
while (i + N <= a.len) : (i += N) {
const va: @Vector(N, f32) = a[i..][0..N].*;
const vb: @Vector(N, f32) = b[i..][0..N].*;
acum += va * vb;
}
var resultado = @reduce(.Add, acum);
while (i < a.len) : (i += 1) {
resultado += a[i] * b[i];
}
return resultado;
}
Boas Práticas
- Use potências de 2 para o tamanho do vetor: 4, 8 ou 16 mapeiam melhor para registradores SIMD reais (128-bit, 256-bit, 512-bit).
- Verifique o target com
@import("builtin"): Para escrever código portável, cheque a arquitetura e ajuste o tamanho do vetor de acordo. - Meça antes de otimizar: O compilador LLVM frequentemente auto-vetoriza loops simples. Meça se a versão manual SIMD é realmente mais rápida antes de aumentar a complexidade.
- Alinhe os dados de entrada: Use
allocator.alignedAlloc(f32, 16, n)para garantir alinhamento adequado, especialmente quando os dados serão processados repetidamente.
Armadilhas Comuns
- Tamanho deve ser potência de 2: Vetores com tamanhos como 3 ou 5 podem não mapear para instruções SIMD reais.
- Alinhamento: Vetores SIMD exigem alinhamento específico. O compilador gerencia isso automaticamente em variáveis locais.
- Fallback escalar: Se a CPU não suportar SIMD para o tamanho escolhido, o compilador gera código escalar equivalente (mais lento).
- Elementos restantes: Quando o array não é múltiplo do tamanho do vetor, trate os elementos restantes em um loop escalar.
Termos Relacionados
- Comptime — Tamanho do vetor é definido em comptime
- Type Coercion — Conversão entre vetores e arrays
- Release Modes — Otimizações SIMD dependem do modo de build
- Cross-Compilation — Instruções SIMD variam por arquitetura