Skip to content

lang::simd

Fixed-width lane vectors: Simd<T, N> over i8 through u64, f32, or f64 and its bool form Mask<N>. Lane-wise arithmetic and bitwise operators, saturating and fused operations, lane comparisons, bitmasks, conversions, shuffles, select, reductions, gathers, and windows over a sequence, with the same bits on every tier (GT0089).

Building a vector

Simd<T, N> holds N lanes of one scalar type. The lane type is i8, u8, i16, u16, i32, u32, i64, u64, f32, or f64, and N is 2, 4, or 8, or also 16 for lanes of 32 bits or fewer. Mask<N> is the bool form, and it also takes 16 lanes. Any other lane type or count reports GT0089.

fn main() {
    let a = Simd::from_array([1.0, 2.0, 3.0, 4.0])
    let b: Simd<f64, 4> = Simd::splat(0.5)
    let xs = #[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
    let window: Simd<f64, 4> = Simd::load(xs, 4)
    println("{:?} {:?}", (a + b).to_array(), window.to_array())  // [1.5, 2.5, 3.5, 4.5] [5.0, 6.0, 7.0, 8.0]
}

Simd::from_array takes the lane count from the array's length. Simd::splat and Simd::load take it from the type the context expects - an annotated let, a parameter, or a return type.

Lane-wise operators

Operator Lane types
+, -, * float and integer
/ float
+%, -%, *%, <<, >> integer
&, \|, ^ integer, and Mask

Both operands share one vector type, and the result has it too. An operator the lane type does not define reports GT0089. The integer lanes wrap under +%, -%, and *% exactly as a scalar of the lane type does.

Methods

Method Answers
to_array() [T; N], the lanes in order
min(other), max(other) the smaller or larger of each pair of lanes
abs() each lane's absolute value
sqrt() each lane's square root; float lanes
lanes_eq, lanes_ne, lanes_lt, lanes_le, lanes_gt, lanes_ge a Mask<N>, true where the comparison holds
reduce_sum(), reduce_min(), reduce_max() one lane value
reduce_and(), reduce_or() the bitwise fold of integer lanes, or bool on a Mask
saturating_add(other), saturating_sub(other) integer lanes clamped to the lane type's range
abs_diff(other) each pair's distance, in the unsigned lane type of the same width
mul_add(a, b) v * a + b per lane with one rounding; float lanes
cast::<U>() each lane converted as as converts it
to_bits(), Simd::from_bits(v) the lanes' bits as unsigned integers, and back
widen_low(), widen_high() half the lanes in the type twice as wide
a.narrow(b) the lanes of a then b in the type half as wide, integers clamped
swizzle([i, ..]), a.concat_swizzle(b, [i, ..]) the lanes the literal indices pick, from v or from a then b
a.interleave(b) the lanes of a and b alternating, as two vectors
swizzle_dyn(idx) u8 lanes: lane idx[i] of v, or zero past the end (a table lookup)
v.store(&mut xs, offset) writes the lanes into xs from offset
v.store_prefix(&mut xs, offset, n) writes the first n lanes

A Mask<N> answers select(if_true, if_false) (each lane from if_true where the mask is true, else from if_false), any(), all(), to_bitmask() (a u64 with lane i at bit i), and first_set() (the lowest true lane, as an Option<i64>); Mask::from_bitmask(bits) builds one.

reduce_sum folds its lanes in one fixed pairing order, so a floating-point sum answers the same bits on the bytecode VM, the JIT, and a native build.

Windows over a sequence

Simd::load(xs, offset) reads N lanes of a Vec, a slice, or a fixed array, and v.store(&mut xs, offset) writes them back through &mut. The window is checked once: an offset whose window reaches past either end panics with the same message on every tier, before any lane is read or written. Simd::load_or(xs, offset, fill) never panics: lanes outside xs read fill, so the tail of a sequence takes the same path as its body, and Simd::gather(xs, indices) reads the lanes the index vector names.

fn halve_in_windows(data: Vec<f64>) -> Vec<f64> {
    let mut out = data
    let halves: Simd<f64, 4> = Simd::splat(0.5)
    let mut offset = 0
    while offset + 4 <= out.len() {
        let window: Simd<f64, 4> = Simd::load(out, offset)
        (window * halves).store(&mut out, offset)
        offset += 4
    }
    out
}

fn main() {
    println("{:?}", halve_in_windows(#[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]))
}

Scanning bytes

A lane comparison read through a mask terminal compiles to vector code in a native build: the scan below tests sixteen bytes per step.

fn find_byte(hay: [u8], needle: u8) -> Option<i64> {
    let target: Simd<u8, 16> = Simd::splat(needle)
    let mut i = 0
    while i < hay.len() {
        let block: Simd<u8, 16> = Simd::load_or(hay, i, needle +% 1)
        if let Some(lane) = block.lanes_eq(target).first_set() {
            return Some(i + lane)
        }
        i += 16
    }
    None
}

fn main() {
    let text = "find the comma, past sixteen bytes"
    println(find_byte(text.bytes(), 44))  // Some(14)
}

Generic over the lane count

A function may take and answer vectors over a const generic lane count, and each count it is called with is its own specialisation:

fn dot<const N: usize>(a: Simd<f64, N>, b: Simd<f64, N>) -> f64 {
    (a * b).reduce_sum()
}

fn main() {
    let small = Simd::from_array([1.0, 2.0])
    let wide = Simd::from_array([1.0, 2.0, 3.0, 4.0])
    println("{} {}", dot(small, small), dot(wide, wide))  // 5 30
}

In the REPL, %info Simd and %info Mask list every method with its signature, and %explain on a binding shows the methods with its own lane type. See examples/simd_lanes.gos for a complete program.