A vectorização SIMD é uma das técnicas mais poderosas para otimização de performance em computação moderna. Com SIMD (Single Instruction Multiple Data), você processa múltiplos dados em paralelo usando instruções de CPU especializadas — obtendo ganhos de 4x a 16x em operações matemáticas vetoriais.
Neste guia completo, você aprenderá a usar SIMD em Zig de forma prática e segura, desde conceitos básicos até otimizações avançadas com AVX2, SSE e NEON.
O que é SIMD?
SIMD permite que uma única instrução de CPU opere em múltiplos dados simultaneamente:
Modo escalar (sem SIMD): Modo vetorial (com SIMD):
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┬─────┬─────┬─────┐
│ a₀ │ │ a₁ │ │ a₂ │ │ a₀ │ a₁ │ a₂ │ a₃ │
└─────┘ └─────┘ └─────┘ └─────┴─────┴─────┴─────┘
+ + + + + + + (1 instrução!)
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┬─────┬─────┬─────┐
│ b₀ │ │ b₁ │ │ b₂ │ │ b₀ │ b₁ │ b₂ │ b₃ │
└─────┘ └─────┘ └─────┘ └─────┴─────┴─────┴─────┘
↓ ↓ ↓ ↓ ↓ ↓ ↓
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┬─────┬─────┬─────┴─────┐
│ c₀ │ │ c₁ │ │ c₂ │ │ c₀ │ c₁ │ c₂ │ c₃ │
└─────┘ └─────┘ └─────┘ └─────┴─────┴─────┴───────────┘
3 operações 1 operação
(3 ciclos) (1 ciclo)
Instruções SIMD por Arquitetura
| Arquitetura | Registradores | Largura | Exemplo de CPUs |
|---|---|---|---|
| SSE | xmm0-15 | 128 bits (4 x f32) | Intel Core 2000+ |
| AVX | ymm0-15 | 256 bits (8 x f32) | Intel Sandy Bridge+ |
| AVX2 | ymm0-15 | 256 bits | Intel Haswell+ |
| AVX-512 | zmm0-31 | 512 bits (16 x f32) | Intel Xeon Skylake+ |
| NEON | v0-31 | 128 bits | ARM Cortex-A, Apple M |
| WASM SIMD | - | 128 bits | Navegadores modernos |
SIMD em Zig: O Básico
Zig oferece suporte SIMD através do tipo @Vector — um vetor de tipos primitivos que o compilador mapeia para instruções SIMD da arquitetura alvo.
Criando Vetores SIMD
const std = @import("std");
pub fn main() !void {
// Vetor de 4 floats (128 bits - SSE/NEON)
const vec4f = @Vector(4, f32);
// Criar vetor a partir de array
const a = vec4f{ 1.0, 2.0, 3.0, 4.0 };
const b = vec4f{ 5.0, 6.0, 7.0, 8.0 };
// Operações aritméticas vetoriais (SIMD automático!)
const sum = a + b; // {6.0, 8.0, 10.0, 12.0}
const product = a * b; // {5.0, 12.0, 21.0, 32.0}
const diff = b - a; // {4.0, 4.0, 4.0, 4.0}
const quotient = b / a; // {5.0, 3.0, 2.333, 2.0}
std.debug.print("Soma: {d}\n", .{sum});
}
Operações SIMD Suportadas
const Vec4f = @Vector(4, f32);
fn simdOperations(a: Vec4f, b: Vec4f) void {
// Aritmética
const add = a + b; // Adição elemento a elemento
const sub = a - b; // Subtração
const mul = a * b; // Multiplicação
const div = a / b; // Divisão
// @abs, @sqrt, @sin, @cos, @exp, @log trabalham em vetores!
const abs_a = @abs(a);
const sqrt_a = @sqrt(a);
// Comparações (retornam vetores de bool)
const gt = a > b; // {true, false, false, false} se a[0] > b[0]
const eq = a == b;
// Máximo e mínimo por elemento
const max = @max(a, b);
const min = @min(a, b);
}
Exemplo Prático: Soma de Arrays com SIMD
Vamos comparar implementação escalar vs SIMD:
const std = @import("std");
const time = std.time;
// Versão escalar tradicional
fn sumScalar(a: []const f32, b: []const f32, result: []f32) void {
for (a, b, result) |va, vb, *vr| {
vr.* = va + vb;
}
}
// Versão SIMD otimizada
fn sumSimd(a: []const f32, b: []const f32, result: []f32) void {
const Vec = @Vector(8, f32); // 8 floats = 256 bits (AVX)
const len = a.len;
const simd_len = len - (len % 8); // Múltiplo de 8
var i: usize = 0;
// Processa 8 elementos de cada vez
while (i < simd_len) : (i += 8) {
const vec_a: Vec = a[i..][0..8].*;
const vec_b: Vec = b[i..][0..8].*;
const vec_sum = vec_a + vec_b;
result[i..][0..8].* = vec_sum;
}
// Resto (elementos que sobram)
while (i < len) : (i += 1) {
result[i] = a[i] + b[i];
}
}
pub fn main() !void {
const allocator = std.heap.page_allocator;
const n = 10_000_000; // 10 milhões de elementos
// Aloca arrays alinhados para SIMD
const a = try allocator.alignedAlloc(f32, 32, n);
const b = try allocator.alignedAlloc(f32, 32, n);
const result_scalar = try allocator.alignedAlloc(f32, 32, n);
const result_simd = try allocator.alignedAlloc(f32, 32, n);
defer allocator.free(a);
defer allocator.free(b);
defer allocator.free(result_scalar);
defer allocator.free(result_simd);
// Inicializa com dados
for (a, 0..) |*val, i| val.* = @floatFromInt(i);
for (b, 0..) |*val, i| val.* = @floatFromInt(i * 2);
// Benchmark escalar
var timer = try time.Timer.start();
sumScalar(a, b, result_scalar);
const scalar_time = timer.read();
// Benchmark SIMD
timer.reset();
sumSimd(a, b, result_simd);
const simd_time = timer.read();
std.debug.print("Scalar: {d} ms\n", .{scalar_time / time.ns_per_ms});
std.debug.print("SIMD: {d} ms\n", .{simd_time / time.ns_per_ms});
std.debug.print("Speedup: {d:.1}x\n", .{@as(f64, @floatFromInt(scalar_time)) / @as(f64, @floatFromInt(simd_time))});
}
Resultados Típicos
Scalar: 45 ms
SIMD: 6 ms
Speedup: 7.5x
Resultados variam por CPU. AVX-512 pode alcançar 10x-16x.
Alinhamento de Memória para SIMD
SIMD requer dados alinhados em memória para máxima performance:
// ❌ Mal: Alinhamento padrão (pode causar falhas ou penalidade)
var buffer: [256]f32 = undefined;
// ✅ Bom: Alinhado para 32 bytes (AVX)
var buffer align(32): [256]f32 = undefined;
// ✅ Alocação dinâmica alinhada
const vec = try allocator.alignedAlloc(f32, 32, 1024);
defer allocator.free(vec);
Detecção de Suporte SIMD
const std = @import("std");
const builtin = @import("builtin");
fn detectSimdSupport() void {
const target = builtin.target;
std.debug.print("Arquitetura: {s}\n", .{@tagName(target.cpu.arch)});
// Verifica features da CPU
const cpu_features = target.cpu.features;
if (target.cpu.arch == .x86_64) {
// SSE2 é baseline em x86_64
std.debug.print("SSE2: ✅ sempre disponível\n");
// AVX reter feature check
// Nota: Em runtime, use CPUID
}
if (target.cpu.arch == .aarch64) {
std.debug.print("NEON: ✅ sempre disponível em AArch64\n");
}
}
Otimizações Avançadas
1. Loop Unrolling com SIMD
fn sumUnrolled(a: []const f32, b: []const f32, result: []f32) void {
const Vec = @Vector(8, f32);
const len = a.len;
const block_size = 32; // 4 vetores de 8 floats
const block_len = len - (len % block_size);
var i: usize = 0;
while (i < block_len) : (i += block_size) {
// Processa 32 elementos em 4 operações SIMD
const v0a: Vec = a[i + 0..][0..8].*;
const v0b: Vec = b[i + 0..][0..8].*;
result[i + 0..][0..8].* = v0a + v0b;
const v1a: Vec = a[i + 8..][0..8].*;
const v1b: Vec = b[i + 8..][0..8].*;
result[i + 8..][0..8].* = v1a + v1b;
const v2a: Vec = a[i + 16..][0..8].*;
const v2b: Vec = b[i + 16..][0..8].*;
result[i + 16..][0..8].* = v2a + v2b;
const v3a: Vec = a[i + 24..][0..8].*;
const v3b: Vec = b[i + 24..][0..8].*;
result[i + 24..][0..8].* = v3a + v3b;
}
// Resto
while (i < len) : (i += 1) {
result[i] = a[i] + b[i];
}
}
2. FMA (Fused Multiply-Add)
// FMA: a * b + c em uma única instrução
// Exemplo: Produto interno
fn dotProductFma(a: []const f32, b: []const f32) f32 {
const Vec = @Vector(8, f32);
var sum_vec: Vec = @splat(0.0);
const len = a.len;
const simd_len = len - (len % 8);
var i: usize = 0;
while (i < simd_len) : (i += 8) {
const va: Vec = a[i..][0..8].*;
const vb: Vec = b[i..][0..8].*;
sum_vec = @mulAdd(Vec, va, vb, sum_vec); // FMA!
}
// Redução horizontal
const sum = @reduce(.Add, sum_vec);
// Resto
var final_sum = sum;
while (i < len) : (i += 1) {
final_sum += a[i] * b[i];
}
return final_sum;
}
3. Operações de Redução Horizontal
fn horizontalOperations() void {
const Vec = @Vector(8, f32);
const v = Vec{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 };
// Soma todos os elementos
const sum = @reduce(.Add, v); // 36.0
// Produto de todos os elementos
const prod = @reduce(.Mul, v); // 40320.0
// Mínimo e máximo
const min = @reduce(.Min, v); // 1.0
const max = @reduce(.Max, v); // 8.0
std.debug.print("Soma: {d}, Produto: {d}\n", .{sum, prod});
}
Caso Real: Processamento de Imagem
// Converte imagem RGB para escala de cinza usando SIMD
fn rgbToGrayscaleSimd(
rgb: []const u8, // Entrada: pixels RGB intercalados
gray: []u8, // Saída: pixels em escala de cinza
pixel_count: usize, // Número de pixels
) void {
// Processa 16 pixels (48 bytes RGB) por iteração
const Vec16u8 = @Vector(16, u8);
const Vec16u16 = @Vector(16, u16);
const weights_r: Vec16u8 = @splat(77); // 0.299 * 256
const weights_g: Vec16u8 = @splat(150); // 0.587 * 256
const weights_b: Vec16u8 = @splat(29); // 0.114 * 256
var i: usize = 0;
const simd_pixels = pixel_count - (pixel_count % 16);
while (i < simd_pixels) : (i += 16) {
// Carrega 16 pixels RGB (requer 3 loads para RGB intercalado)
// Simplificado para demonstração
const r = Vec16u8{ rgb[i*3+0], rgb[i*3+3], rgb[i*3+6], ... };
const g = Vec16u8{ rgb[i*3+1], rgb[i*3+4], ... };
const b = Vec16u8{ rgb[i*3+2], rgb[i*3+5], ... };
// Cálculo ponderado: gray = (r*77 + g*150 + b*29) >> 8
const gray16 = (@as(Vec16u16, r) * @as(Vec16u16, weights_r) +
@as(Vec16u16, g) * @as(Vec16u16, weights_g) +
@as(Vec16u16, b) * @as(Vec16u16, weights_b)) >> @splat(8);
gray[i..][0..16].* = @truncate(gray16);
}
// Resto escalar
while (i < pixel_count) : (i += 1) {
const r = @as(u16, rgb[i * 3 + 0]);
const g = @as(u16, rgb[i * 3 + 1]);
const b = @as(u16, rgb[i * 3 + 2]);
gray[i] = @truncate((r * 77 + g * 150 + b * 29) >> 8);
}
}
Compilando com Otimizações SIMD
# Habilita features específicas da CPU
zig build-exe main.zig -O ReleaseFast -mcpu=sandybridge+avx2
# Target genérico com SSE2 (padrão em x86_64)
zig build-exe main.zig -O ReleaseFast -mcpu=x86_64
# Target native (usa todas as features da CPU local)
zig build-exe main.zig -O ReleaseFast -mcpu=native
# Para WebAssembly com SIMD
zig build-exe main.zig -O ReleaseFast -target wasm32-wasi -mcpu=bleeding_edge
Melhores Práticas
✅ Faça
// ✅ Use alinhamento adequado
const aligned = try allocator.alignedAlloc(f32, 32, size);
// ✅ Processa em blocos, depois o resto
const simd_count = total - (total % vec_len);
// ... SIMD loop ...
// ... scalar remainder ...
// ✅ Use @reduce para operações horizontais
const sum = @reduce(.Add, vec);
// ✅ Use @mulAdd para FMA quando disponível
sum = @mulAdd(Vec, a, b, sum);
❌ Evite
// ❌ Acesso não alinhado pode causar falhas ou penalidade
const ptr: [*]align(1) f32 = @ptrCast(unaligned_ptr);
// ❌ Branches dentro de loops SIMD
if (vec[i] > 0) { // Isso desvetoriza!
// ...
}
// ❌ Conversoes frequentes entre tipos
const f: f32 = @floatFromInt(@as(u8, vec[i]));
Benchmarks Comparativos
| Operação | Escalar | SIMD 256b | Speedup |
|---|---|---|---|
| Soma de arrays | 1x | 7-8x | 7-8x |
| Produto interno | 1x | 6-7x | 6-7x |
| Multiplicação matriz | 1x | 4-6x | 4-6x |
| Conversão RGB→Gray | 1x | 8-10x | 8-10x |
| Filtro media móvel | 1x | 5-7x | 5-7x |
Exercícios
**Exercício 1:** Implemente soma de matrizes 4x4 usando SIMD
fn addMatrix4x4(a: [16]f32, b: [16]f32) [16]f32 {
const Vec4 = @Vector(4, f32);
var result: [16]f32 = undefined;
// Cada linha da matriz usa um vetor de 4 floats
inline for (0..4) |row| {
const va: Vec4 = a[row*4..][0..4].*;
const vb: Vec4 = b[row*4..][0..4].*;
result[row*4..][0..4].* = va + vb;
}
return result;
}
**Exercício 2:** Implemente normalização vetorial usando SIMD
fn normalize4(v: @Vector(4, f32)) @Vector(4, f32) {
const squared = v * v;
const sum_sq = @reduce(.Add, squared);
const inv_len = 1.0 / @sqrt(sum_sq);
return v * @as(@Vector(4, f32), @splat(inv_len));
}
**Exercício 3:** Implemente busca de máximo em array com SIMD
Dica: Use @reduce(.Max, vec) em blocos, depois reduza os máximos parciais.
Próximos Passos
Agora que você domina SIMD em Zig:
- 🔢 Comptime em Zig — Aprenda metaprogramação para gerar código SIMD especializado em tempo de compilação
- 🧠 Zig para Iniciantes — Série completa de conceitos fundamentais
- ⚡ Otimização de Performance em Zig — Técnicas avançadas de profiling e otimização
- 🎮 Zig para Game Development — Aplique SIMD em engines de jogos
- 🔗 Zig vs C++ — Compare estratégias SIMD entre linguagens
Recursos Adicionais
- Intel Intrinsics Guide — Referência de instruções SIMD
- Agner Fog’s Optimization Manuals — Bíblia de otimização de assembly
- Zig Language Reference - Vectors
Qual é seu caso de uso para SIMD? Compartilhe suas otimizações na seção de comentários!