Zig SIMD: Guia Completo de Vectorização

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

ArquiteturaRegistradoresLarguraExemplo de CPUs
SSExmm0-15128 bits (4 x f32)Intel Core 2000+
AVXymm0-15256 bits (8 x f32)Intel Sandy Bridge+
AVX2ymm0-15256 bitsIntel Haswell+
AVX-512zmm0-31512 bits (16 x f32)Intel Xeon Skylake+
NEONv0-31128 bitsARM Cortex-A, Apple M
WASM SIMD-128 bitsNavegadores 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çãoEscalarSIMD 256bSpeedup
Soma de arrays1x7-8x7-8x
Produto interno1x6-7x6-7x
Multiplicação matriz1x4-6x4-6x
Conversão RGB→Gray1x8-10x8-10x
Filtro media móvel1x5-7x5-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:

  1. 🔢 Comptime em Zig — Aprenda metaprogramação para gerar código SIMD especializado em tempo de compilação
  2. 🧠 Zig para Iniciantes — Série completa de conceitos fundamentais
  3. Otimização de Performance em Zig — Técnicas avançadas de profiling e otimização
  4. 🎮 Zig para Game Development — Aplique SIMD em engines de jogos
  5. 🔗 Zig vs C++ — Compare estratégias SIMD entre linguagens

Recursos Adicionais


Qual é seu caso de uso para SIMD? Compartilhe suas otimizações na seção de comentários!

Continue aprendendo Zig

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