zig-skills/references/std-simd.md

17 KiB

std.simd

SIMD (Single Instruction, Multiple Data) utilities for parallel processing of multiple elements at once. Provides convenience functions for vector manipulation, pattern generation, searching, and parallel computation.

Quick Reference

Category Functions
Vector Length suggestVectorLength, suggestVectorLengthForCpu, VectorIndex, VectorCount
Pattern Generation iota, repeat, join, interlace, deinterlace
Extraction/Shifting extract, mergeShift, shiftElementsLeft, shiftElementsRight
Rotation/Reversal rotateElementsLeft, rotateElementsRight, reverseOrder
Searching firstTrue, lastTrue, countTrues, firstIndexOfValue, lastIndexOfValue, countElementsWithValue
Parallel Scans prefixScan, prefixScanWithFunc

Core Concepts

Vector Types

Zig vectors are first-class types declared with @Vector(len, T). Element types can be booleans, integers, floats, or pointers:

const Vec4 = @Vector(4, f64);      // 4 f64 values
const Vec8i = @Vector(8, i32);     // 8 i32 values
const Vec16b = @Vector(16, bool);  // 16 booleans
const Vec4p = @Vector(4, *u8);     // 4 pointers

Vector length limits: Zig supports lengths up to 2^32-1, but powers of two from 2-64 are typical. Excessively long vectors (e.g., 2^20) may crash the compiler.

Compilation behavior: Vectors shorter than the native SIMD size compile to single instructions. Longer vectors compile to multiple SIMD instructions. Without SIMD support, operations fall back to element-by-element execution.

Built-in Operations

Vectors support arithmetic, comparisons, and builtins directly (all element-wise):

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 };

// Arithmetic (element-wise)
const sum = a + b;      // { 6.0, 8.0, 10.0, 12.0 }
const prod = a * b;     // { 5.0, 12.0, 21.0, 32.0 }

// Comparison (returns bool vector)
const mask = a < b;     // { true, true, true, true }

// Broadcast scalar to all lanes
const twos: @Vector(4, f32) = @splat(2.0);  // { 2.0, 2.0, 2.0, 2.0 }

// Math builtins (hardware-accelerated when available)
const sines = @sin(a);
const sqrts = @sqrt(a);

// Horizontal reduction
const total = @reduce(.Add, a);  // 10.0
const max_val = @reduce(.Max, a);  // 4.0

Important: and and or keywords do NOT work on bool vectors (they affect control flow). Use & and | bitwise operators, or @select instead.

Vector-Compatible Builtins

These builtins work element-wise on vectors:

Category Builtins
Math @sqrt, @sin, @cos, @exp, @exp2, @log, @log2, @log10
Rounding @floor, @ceil, @trunc, @round
Arithmetic @abs, @min, @max, @mulAdd, @divFloor, @divTrunc, @mod, @rem
Bit ops @clz, @ctz, @popCount, @byteSwap, @bitReverse
Overflow @addWithOverflow, @subWithOverflow, @mulWithOverflow, @shlWithOverflow

Array/Slice Conversion

// Array to vector (automatic)
const arr: [4]f32 = .{ 1.1, 3.2, 4.5, 5.6 };
const vec: @Vector(4, f32) = arr;

// Vector to array (automatic)
const arr2: [4]f32 = vec;

// Slice with comptime-known length to vector
const vec2: @Vector(2, f32) = arr[1..3].*;

// Runtime offset with comptime length
const slice: []const f32 = &arr;
var offset: usize = 1;
const vec3: @Vector(2, f32) = slice[offset..][0..2].*;

Vector Destructuring

Vectors can be destructured like tuples:

const vec: @Vector(4, f32) = .{ 1.0, 2.0, 3.0, 4.0 };
const a, const b, _, _ = vec;  // a=1.0, b=2.0, ignore rest

// Useful for SIMD unpacking (emulating punpckldq)
pub fn unpack(x: @Vector(4, f32), y: @Vector(4, f32)) @Vector(4, f32) {
    const a, const c, _, _ = x;
    const b, const d, _, _ = y;
    return .{ a, b, c, d };
}

@shuffle - Rearrange Elements

Rearrange elements from one or two vectors using an index mask:

const a: @Vector(7, u8) = .{ 'o', 'l', 'h', 'e', 'r', 'z', 'w' };
const b: @Vector(4, u8) = .{ 'w', 'd', '!', 'x' };

// Shuffle within single vector (pass undefined as second)
const mask1: @Vector(5, i32) = .{ 2, 3, 1, 1, 0 };
const hello: @Vector(5, u8) = @shuffle(u8, a, undefined, mask1);
// "hello"

// Combine two vectors (negative indices select from b: -1=b[0], -2=b[1], etc.)
const mask2: @Vector(6, i32) = .{ -1, 0, 4, 1, -2, -3 };
const world: @Vector(6, u8) = @shuffle(u8, a, b, mask2);
// "world!"

@select - Conditional Selection

Select elements from two vectors based on a bool mask:

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 };
const mask: @Vector(4, bool) = .{ true, false, true, false };

const result = @select(f32, mask, a, b);
// { 1.0, 6.0, 3.0, 8.0 }  (a where true, b where false)

@reduce - Horizontal Reduction

Reduce a vector to a scalar using an operation:

const vec: @Vector(4, i32) = .{ 1, 2, 3, 4 };

// Arithmetic reductions
const sum = @reduce(.Add, vec);  // 10
const prod = @reduce(.Mul, vec); // 24
const min_val = @reduce(.Min, vec);  // 1
const max_val = @reduce(.Max, vec);  // 4

// Bitwise reductions
const and_val = @reduce(.And, vec);  // 0
const or_val = @reduce(.Or, vec);    // 7
const xor_val = @reduce(.Xor, vec);  // 4

// Boolean reductions (for bool vectors)
const mask: @Vector(4, bool) = .{ true, true, false, true };
const all_true = @reduce(.And, mask);  // false
const any_true = @reduce(.Or, mask);   // true

Available operations by type:

  • Integers: All operations (Add, Mul, Min, Max, And, Or, Xor)
  • Floats: Add, Mul, Min, Max
  • Booleans: And, Or, Xor

Optimal Vector Length

suggestVectorLength

Query the suggested vector length for the current target. This is a comptime heuristic: its element type must be comptime-known.

const std = @import("std");

// Get optimal lane count for this type on current hardware
const len = std.simd.suggestVectorLength(f32) orelse 4;

// Use comptime to create vector type
const Vec = @Vector(len, f32);

Returns null if scalars are recommended (no SIMD benefit).

suggestVectorLengthForCpu

Query the suggested length for a specific CPU target. Both the element type and std.Target.Cpu argument are comptime parameters:

const len = std.simd.suggestVectorLengthForCpu(f64, target_cpu) orelse 2;

Architecture support:

  • x86: SSE (128-bit), AVX2 (256-bit), AVX-512 (512-bit)
  • ARM: NEON (128-bit)
  • AArch64: SVE (256-bit heuristic), otherwise NEON (128-bit)
  • RISC-V: V extension (32-bit to 65536-bit via zvl* features)
  • WebAssembly: simd128 (128-bit)
  • PowerPC: AltiVec (128-bit)

Vector Index/Count Types

Get the smallest integer type for indexing or counting:

const Vec8 = @Vector(8, u32);

// Type that can index any element (0-7)
const Idx = std.simd.VectorIndex(Vec8);  // u3

// Type that can hold the count (0-8)
const Cnt = std.simd.VectorCount(Vec8);  // u4

Pattern Generation

iota - Sequential Values

Generate a vector of sequential values starting from 0:

const indices = std.simd.iota(i32, 8);
// { 0, 1, 2, 3, 4, 5, 6, 7 }

const floats = std.simd.iota(f32, 4);
// { 0.0, 1.0, 2.0, 3.0 }

repeat - Repeating Pattern

Repeat a smaller vector/array to fill a larger one:

const pattern = [_]u32{ 1, 2, 3 };
const repeated = std.simd.repeat(8, pattern);
// { 1, 2, 3, 1, 2, 3, 1, 2 }

const vec: @Vector(2, f32) = .{ 10.0, 20.0 };
const tiled = std.simd.repeat(6, vec);
// { 10.0, 20.0, 10.0, 20.0, 10.0, 20.0 }

join - Concatenate Vectors

Concatenate two vectors end-to-end:

const a: @Vector(4, u32) = .{ 10, 20, 30, 40 };
const b: @Vector(4, u32) = .{ 55, 66, 77, 88 };
const joined = std.simd.join(a, b);
// { 10, 20, 30, 40, 55, 66, 77, 88 }

interlace - Interleave Multiple Vectors

Alternate elements from multiple vectors:

const a: @Vector(4, u32) = .{ 10, 20, 30, 40 };
const b: @Vector(4, u32) = .{ 55, 66, 77, 88 };
const interleaved = std.simd.interlace(.{ a, b });
// { 10, 55, 20, 66, 30, 77, 40, 88 }

// Works with more than 2 vectors
const v1: @Vector(2, u8) = .{ 0, 1 };
const v2: @Vector(2, u8) = .{ 2, 3 };
const v3: @Vector(2, u8) = .{ 4, 5 };
const result = std.simd.interlace(.{ v1, v2, v3 });
// { 0, 2, 4, 1, 3, 5 }

Note: Does not work on MIPS (compile error).

deinterlace - Split Interleaved Data

Reverse of interlace - split into separate vectors:

const interleaved: @Vector(8, u32) = .{ 10, 55, 20, 66, 30, 77, 40, 88 };
const result = std.simd.deinterlace(2, interleaved);
// result[0] = { 10, 20, 30, 40 }
// result[1] = { 55, 66, 77, 88 }

Element Extraction and Shifting

extract - Get Subvector

Extract a contiguous slice of elements. first and count are comptime-known:

const vec: @Vector(8, u32) = .{ 0, 1, 2, 3, 4, 5, 6, 7 };
const slice = std.simd.extract(vec, 2, 3);
// { 2, 3, 4 }

shiftElementsLeft / shiftElementsRight

Shift elements, filling with a value. The shift amount is comptime-known:

const vec: @Vector(4, u32) = .{ 10, 20, 30, 40 };

// Shift left (toward lower indices), fill from right
const left = std.simd.shiftElementsLeft(vec, 2, 999);
// { 30, 40, 999, 999 }

// Shift right (toward higher indices), fill from left
const right = std.simd.shiftElementsRight(vec, 2, 999);
// { 999, 999, 10, 20 }

rotateElementsLeft / rotateElementsRight

Circular rotation (elements wrap around). The rotation amount is comptime-known:

const vec: @Vector(4, u32) = .{ 10, 20, 30, 40 };

const rotl = std.simd.rotateElementsLeft(vec, 1);
// { 20, 30, 40, 10 }

const rotr = std.simd.rotateElementsRight(vec, 1);
// { 40, 10, 20, 30 }

reverseOrder

Reverse element order:

const vec: @Vector(4, u32) = .{ 10, 20, 30, 40 };
const reversed = std.simd.reverseOrder(vec);
// { 40, 30, 20, 10 }

mergeShift

Combine two vectors and extract a shifted window. The shift amount is comptime-known:

const a: @Vector(4, u32) = .{ 1, 2, 3, 4 };
const b: @Vector(4, u32) = .{ 5, 6, 7, 8 };
const merged = std.simd.mergeShift(a, b, 2);
// Joins to { 1, 2, 3, 4, 5, 6, 7, 8 }, extracts starting at index 2
// { 3, 4, 5, 6 }

Searching

firstTrue / lastTrue

Find first/last true element in a boolean vector:

const mask: @Vector(8, bool) = .{ false, false, true, false, true, false, false, false };

const first = std.simd.firstTrue(mask);  // 2
const last = std.simd.lastTrue(mask);    // 4

// Returns null if no true values
const all_false: @Vector(4, bool) = .{ false, false, false, false };
const none = std.simd.firstTrue(all_false);  // null

countTrues

Count true elements:

const mask: @Vector(8, bool) = .{ true, false, true, false, true, false, true, false };
const count = std.simd.countTrues(mask);  // 4

firstIndexOfValue / lastIndexOfValue

Find first/last occurrence of a value:

const vec: @Vector(8, u32) = .{ 6, 4, 7, 4, 4, 2, 3, 7 };

const first_4 = std.simd.firstIndexOfValue(vec, 4);  // 1
const last_4 = std.simd.lastIndexOfValue(vec, 4);    // 4
const not_found = std.simd.lastIndexOfValue(vec, 99); // null

countElementsWithValue

Count occurrences of a value:

const vec: @Vector(8, u32) = .{ 6, 4, 7, 4, 4, 2, 3, 7 };
const count = std.simd.countElementsWithValue(vec, 4);  // 3

Parallel Prefix Scans

prefixScan

Compute cumulative operations across vector lanes. This is an O(log N), non-linear associative scan; floating-point results can differ from a scalar left-to-right scan because the grouping and rounding differ.

const vec: @Vector(4, i32) = .{ 11, 23, 9, -21 };

// Running sum
const sums = std.simd.prefixScan(.Add, 1, vec);
// { 11, 34, 43, 22 }

// Running product
const prods = std.simd.prefixScan(.Mul, 1, vec);
// { 11, 253, 2277, -47817 }

// Running min
const mins = std.simd.prefixScan(.Min, 1, vec);
// { 11, 11, 9, -21 }

// Running max
const maxs = std.simd.prefixScan(.Max, 1, vec);
// { 11, 23, 23, 23 }

// Bitwise operations
const ands = std.simd.prefixScan(.And, 1, vec);
const ors = std.simd.prefixScan(.Or, 1, vec);
const xors = std.simd.prefixScan(.Xor, 1, vec);

Hop parameter: Controls which elements combine. hop=2 combines every other element.

const vec: @Vector(4, i32) = .{ 11, 23, 9, -21 };
const skip = std.simd.prefixScan(.Add, 2, vec);
// { 11, 23, 20, 2 }  (11+9=20, 23+(-21)=2)

// Negative hop scans in reverse
const rev = std.simd.prefixScan(.Add, -1, vec);
// { 22, 11, -12, -21 }

Note: Does not work on MIPS (compile error).

prefixScanWithFunc

Use a custom associative function. The callback may return an error union; choose ErrorType accordingly and use try on prefixScanWithFunc to propagate callback errors. Passing void selects the non-erroring form:

fn myMax(a: @Vector(4, f32), b: @Vector(4, f32)) @Vector(4, f32) {
    return @max(a, b);
}

const vec: @Vector(4, f32) = .{ 1.0, 5.0, 2.0, 8.0 };
const result = std.simd.prefixScanWithFunc(1, vec, void, myMax, -std.math.inf(f32));
// { 1.0, 5.0, 5.0, 8.0 }

The identity value must satisfy: func(x, identity) == x.

Practical Patterns

Branchless Selection

Replace if statements with vector selection:

// Scalar (branching)
fn clampScalar(x: f32, lo: f32, hi: f32) f32 {
    if (x < lo) return lo;
    if (x > hi) return hi;
    return x;
}

// Vector (branchless)
fn clampSimd(x: @Vector(4, f32), lo: f32, hi: f32) @Vector(4, f32) {
    const lo_vec: @Vector(4, f32) = @splat(lo);
    const hi_vec: @Vector(4, f32) = @splat(hi);
    return @min(@max(x, lo_vec), hi_vec);
}

Convergence Loops

Process lanes that converge at different rates:

fn iterateUntilConverged(vec: @Vector(4, f64), tolerance: f64) @Vector(4, f64) {
    const tol_vec: @Vector(4, f64) = @splat(tolerance);
    var current = vec;
    var converged: @Vector(4, bool) = @splat(false);

    while (!@reduce(.And, converged)) {
        const next = computeNext(current);
        const delta = @abs(next - current);
        converged = delta <= tol_vec;
        current = next;
    }
    return current;
}

Time-Batched Processing

Process multiple time points for one object:

const Vec4 = @Vector(4, f64);

fn propagateV4(state: *const State, times: [4]f64) [4]Result {
    const time_vec: Vec4 = times;
    // Process all 4 times simultaneously
    const positions = computePositions(state, time_vec);
    const velocities = computeVelocities(state, time_vec);
    // ...
}

Object-Batched Processing (Struct of Arrays)

Process multiple objects at the same time point:

// Struct of Arrays layout for 4 objects
const ObjectsV4 = struct {
    x: @Vector(4, f64),
    y: @Vector(4, f64),
    vx: @Vector(4, f64),
    vy: @Vector(4, f64),
};

fn updatePositions(objs: *ObjectsV4, dt: f64) void {
    const dt_vec: @Vector(4, f64) = @splat(dt);
    objs.x += objs.vx * dt_vec;
    objs.y += objs.vy * dt_vec;
}

Custom atan2 Approximation

LLVM lacks vectorized atan2. Implement polynomial approximation:

fn atan2Simd(y: @Vector(4, f64), x: @Vector(4, f64)) @Vector(4, f64) {
    const abs_x = @abs(x);
    const abs_y = @abs(y);
    const max_xy = @max(abs_x, abs_y);
    const min_xy = @min(abs_x, abs_y);
    const epsilon: @Vector(4, f64) = @splat(1.0e-30);
    const t = min_xy / @max(max_xy, epsilon);

    // Polynomial approximation (Horner's method)
    const t2 = t * t;
    var atan_t = @as(@Vector(4, f64), @splat(0.0028662257));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(-0.0161657367));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(0.0429096138));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(-0.0752896400));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(0.1065626393));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(-0.1420889944));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(0.1999355085));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(-0.3333314528));
    atan_t = atan_t * t2 + @as(@Vector(4, f64), @splat(1.0));
    atan_t = atan_t * t;

    // Quadrant correction
    const half_pi: @Vector(4, f64) = @splat(std.math.pi / 2.0);
    const pi: @Vector(4, f64) = @splat(std.math.pi);
    const swap_mask = abs_y > abs_x;
    atan_t = @select(f64, swap_mask, half_pi - atan_t, atan_t);
    const x_neg = x < @as(@Vector(4, f64), @splat(0.0));
    atan_t = @select(f64, x_neg, pi - atan_t, atan_t);
    const y_neg = y < @as(@Vector(4, f64), @splat(0.0));
    return @select(f64, y_neg, -atan_t, atan_t);
}

Performance Notes

  • Optimal vector size: Use suggestVectorLength for portable code; don't hardcode lane counts. Powers of two (2-64) are most efficient
  • Compilation: Lowering depends on the target, vector width, operation, and optimizer; inspect generated code for performance-critical paths
  • Alignment: Vectors are automatically aligned; use @alignCast when loading from byte pointers
  • Branching: Replace scalar branches with @select for branchless SIMD code. and/or keywords don't work on bool vectors
  • Reductions: @reduce operations break SIMD parallelism; minimize their use in hot paths
  • Memory layout: Prefer Struct-of-Arrays over Array-of-Structs for better vectorization
  • Cache tiling: For large datasets, process in cache-sized chunks (e.g., 64 elements) to maintain data locality
  • Fused operations: For floating-point T, use @mulAdd(T, a, b, c) for a fused (a * b) + c with one final rounding
  • MIPS limitation: interlace and prefixScan don't work on MIPS architecture