Zig SIMD and Vectors
SIMD stands for Single Instruction, Multiple Data. A single SIMD instruction processes multiple values in parallel using wide CPU registers. Zig exposes SIMD through the @Vector type, letting you write portable vector code that the compiler maps to the best available CPU instructions — SSE, AVX, NEON, or others depending on the target.
The Concept
Scalar (normal) addition — one operation per number: a[0]+b[0]=c[0] a[1]+b[1]=c[1] a[2]+b[2]=c[2] a[3]+b[3]=c[3] 4 operations total SIMD addition — one instruction for all: [a0,a1,a2,a3] + [b0,b1,b2,b3] = [c0,c1,c2,c3] 1 operation total Speedup: up to 4× for 4-wide, 8× for 8-wide, 16× for 16-wide
Declaring a Vector Type
@Vector(lane_count, element_type) @Vector(4, f32) ← 4 floats (128-bit SSE register) @Vector(8, f32) ← 8 floats (256-bit AVX register) @Vector(4, i32) ← 4 integers @Vector(16, u8) ← 16 bytes (common for text processing)
Basic Vector Arithmetic
const std = @import("std");
pub fn main() void {
const Vec4f = @Vector(4, f32);
const a = Vec4f{ 1.0, 2.0, 3.0, 4.0 };
const b = Vec4f{ 10.0, 20.0, 30.0, 40.0 };
const sum = a + b; // [11, 22, 33, 44]
const diff = b - a; // [9, 18, 27, 36]
const prod = a * b; // [10, 40, 90, 160]
const quot = b / a; // [10, 10, 10, 10]
std.debug.print("sum[0] = {d}\n", .{sum[0]}); // 11
std.debug.print("prod[2] = {d}\n", .{prod[2]}); // 90
}
All standard arithmetic operators work element-wise on vectors. The compiler generates SIMD instructions automatically when available on the target CPU.
Splat — Broadcast a Scalar to All Lanes
const Vec4i = @Vector(4, i32);
// Fill all 4 lanes with the value 7
const sevens: Vec4i = @splat(7);
// sevens = {7, 7, 7, 7}
const data = Vec4i{ 1, 2, 3, 4 };
const scaled = data * sevens;
// scaled = {7, 14, 21, 28}
@splat(7) on Vec4i: ┌───┬───┬───┬───┐ │ 7 │ 7 │ 7 │ 7 │ └───┴───┴───┴───┘
Reduce — Collapse a Vector to a Scalar
const Vec8u = @Vector(8, u32);
const data = Vec8u{ 10, 20, 30, 40, 50, 60, 70, 80 };
const total = @reduce(.Add, data); // 360
const max = @reduce(.Max, data); // 80
const min = @reduce(.Min, data); // 10
const all_or = @reduce(.Or, data); // bitwise OR of all
@reduce(.Add, [10,20,30,40,50,60,70,80]): Step 1: [30, 70, 110, 150] (add pairs) Step 2: [100, 260] (add pairs again) Step 3: 360 (final sum)
Shuffle — Rearrange Vector Elements
const Vec4f = @Vector(4, f32);
const v = Vec4f{ 1.0, 2.0, 3.0, 4.0 };
// Reverse the vector: [4, 3, 2, 1]
const reversed = @shuffle(f32, v, undefined, [4]i32{ 3, 2, 1, 0 });
// Duplicate first two elements: [1, 2, 1, 2]
const dup = @shuffle(f32, v, undefined, [4]i32{ 0, 1, 0, 1 });
std.debug.print("reversed[0] = {d}\n", .{reversed[0]}); // 4
std.debug.print("dup[2] = {d}\n", .{dup[2]}); // 1
Practical Example: Vector Dot Product
const std = @import("std");
fn dotProduct(a: []const f32, b: []const f32) f32 {
const Vec = @Vector(8, f32);
var acc: Vec = @splat(0.0);
const len = a.len;
const chunks = len / 8;
var i: usize = 0;
while (i < chunks * 8) : (i += 8) {
const va: Vec = a[i..][0..8].*;
const vb: Vec = b[i..][0..8].*;
acc += va * vb;
}
var result = @reduce(.Add, acc);
// Handle remaining elements (if len not divisible by 8)
while (i < len) : (i += 1) {
result += a[i] * b[i];
}
return result;
}
pub fn main() void {
const a = [_]f32{ 1, 2, 3, 4, 5, 6, 7, 8 };
const b = [_]f32{ 8, 7, 6, 5, 4, 3, 2, 1 };
const dp = dotProduct(&a, &b);
std.debug.print("Dot product: {d}\n", .{dp});
// 1×8 + 2×7 + 3×6 + 4×5 + 5×4 + 6×3 + 7×2 + 8×1 = 120
}
Converting Between Vectors and Arrays
const Vec4i = @Vector(4, i32);
// Array to vector
const arr = [4]i32{ 1, 2, 3, 4 };
const vec: Vec4i = arr;
// Vector to array
const back: [4]i32 = vec;
std.debug.print("{d}\n", .{back[2]}); // 3
Choosing Vector Width
Common SIMD widths:
128-bit (SSE2/NEON): @Vector(4, f32) @Vector(16, u8) @Vector(2, f64)
256-bit (AVX2): @Vector(8, f32) @Vector(32, u8) @Vector(4, f64)
512-bit (AVX-512): @Vector(16, f32) @Vector(64, u8) @Vector(8, f64)
For portability, use 128-bit vectors — supported on all modern CPUs.
Enable AVX2 in build.zig to unlock 256-bit:
exe.root_module.cpu_features_add.addFeature(
@intFromEnum(std.Target.x86.Feature.avx2)
);
When to Use SIMD
Good candidates: ✓ Processing large arrays of numbers (audio, images, physics) ✓ String scanning and pattern matching ✓ Cryptographic hash functions ✓ Machine learning inference ✓ Sorting and searching large datasets Poor candidates: ✗ Short arrays (overhead of setup exceeds gain) ✗ Irregular data access patterns (random lookups) ✗ Code with many branches per element ✗ Single-value computations
SIMD gives the largest speedups when you process large, regular, flat arrays where the same operation applies to every element. Profile before and after to confirm the gain justifies the added complexity.
