Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.simd — portable explicit lanes

Use this module when an algorithm needs explicit lane operations. Start with ordinary column pipelines for bulk clist queries: their automatic SIMD handles vector widths and tails without changing the source algorithm.

Values and construction

i32x4, u32x4, f32x4, and f64x2 are nominal, copyable values. Their names describe scalar types and logical lane counts, not an instruction set. Construct them with i32x4.Lanes(a, b, c, d) or the matching module factory, for example splat_i32x4(value). Construction, arithmetic, comparison and selection allocate nothing. Values can be passed, returned, nested in other values and stored in collections under the ordinary enum rules.

lane(index) returns Option[Scalar]; indices outside 0..lane_count return Nothing, including negative indices. to_tuple() copies the logical lanes in order, without allocation. Ordinary == compares complete values; use the simd_* methods below when each lane needs its own comparison result.

Construct distinct lanes, then add the same value to each with explicit wrapping:

import std.simd as simd
def main() -> Result[(), IoError]:
    values = simd.u32x4.Lanes(0u32, 1u32, 2u32, u32.MAX)
    print(values.wrapping_add(simd.splat_u32x4(1u32)).to_tuple())?
    Ok(())
(1, 2, 3, 0)

Arithmetic and masks

OperationContract
add, sub, mulInteger vectors return Result[Vector, OverflowError[Lane]]; float vectors stay infallible
Float divScalar IEEE division, including infinities and NaNs
Integer wrapping_add, wrapping_sub, wrapping_mulModular lane arithmetic
Integer wrapping_sum()Modular sum of all lanes
Float sum_ordered(initial)Left fold from initial, in increasing lane order
simd_eq/ne/lt/le/gt/ge(other)Lane comparisons returning a compatible mask

Integer checked operations execute in lane order and return the first overflowing lane’s scalar OverflowError, including its wrapped value. Callers handle this ordinary Result; an unchecked: caller does not change the method’s contract. Floating operations do not implicitly fuse or reassociate. NaN comparisons, signed zero and subnormals follow the target’s scalar rules; NaN payload identity is not promised.

The integer reduction API is explicitly wrapping_sum(): it associates as (((lane0 + lane1) + lane2) + lane3), wrapping each addition. There is no ordinary fallible integer reduction method. Float sum_ordered(initial) also folds left in lane order, without reassociation.

Comparisons on 32-bit lanes produce mask32x4; f64x2 produces mask64x2. Masks contain logical booleans, constructed directly with Lanes(...) or by comparison. There are no integer-mask conversions or public native bit encoding.

mask.select(yes, no) selects each lane from two vectors of the same type and the matching mask shape. It evaluates both operands before selection: it does not suppress side effects, allocation failures or traps while computing them. any(), all() and count() summarize a mask. invert(), and_mask(other), or_mask(other) and xor_mask(other) combine logical lanes.

Select a floor for the lanes below it; the input stays unchanged:

import std.simd as simd
def main() -> Result[(), IoError]:
    values = simd.f32x4.Lanes(18.0f32, 21.0f32, 16.0f32, 24.0f32)
    floor = simd.splat_f32x4(20.0f32)
    below = values.simd_lt(floor)
    print(below.count(), below.select(floor, values).to_tuple())?
    Ok(())
2 (20.0, 21.0, 20.0, 24.0)

Like other generic Plenty helpers, select[T] forwards structurally to yes.selected(mask, no). The module’s vector implementations of that helper are private. A user-defined type may supply its own public selected method; its effects and allocation behavior then belong to that implementation, and the compiler does not replace it with a vector intrinsic.

Safe list access

load_i32x4(values, offset=0) and the corresponding factories for other vector types accept a shared list borrow and return Option[Vector]. They return Nothing unless a whole vector fits. vector.store(&mut values, offset) returns True after replacing all lanes, or False without changing the list if the whole vector does not fit. Negative offsets are invalid. Bounds are checked before adding lane offsets, including for i64.MAX.

These operations neither allocate nor grow the list. They access ordinary list elements individually; lists are not packed native arrays. Returned vectors own copies of their scalar lanes, so no borrow persists after loading. Stores take an exclusive borrow and preserve length and capacity. Process full groups and then the scalar tail explicitly when writing a manual kernel.

Handle a load that may not fit. A failed store leaves every element unchanged:

import std.simd as simd
def main() -> Result[(), AllocError | IoError | OverflowError[i32]]:
    mut values = [1i32, 2i32, 3i32, 4i32]?
    match simd.load_i32x4(values):
        case Some(lanes):
            print(lanes.store(&mut values, 1))?
            print(lanes.add(simd.splat_i32x4(10i32))?.store(&mut values, 0))?
        case Nothing:
            print("A full vector does not fit")?
    print(values)?
    Ok(())
False
True
[11, 12, 13, 14]

Rust relationship and initial limits

The model follows Rust’s portable std::simd: fixed logical lanes, splats, per-lane arithmetic and simd_* comparisons, masks, and selection. This first Plenty module has concrete types rather than Simd[T, N] and uses methods rather than overloaded arithmetic operators. Enum factories use module functions and the Lanes variant because Plenty has no static enum methods. Unlike Rust’s wrapping integer operators, add/sub/mul preserve Plenty’s checked arithmetic; modular operations have explicit wrapping_* names.

The implementation uses existing inline enums, whose payload stores each lane in a tagged scalar slot. A four-lane payload currently occupies 64 bytes, plus the ordinary value handle; this is not a packed 16-byte vector or a native register ABI. Mask variants expose logical booleans, not hardware masks. Neither vector nor mask layout is a C, serialization, Arrow or Parquet contract.

On x86-64, the default optimized profile lowers wrapping arithmetic, float arithmetic, comparisons and selection to baseline 128-bit SIMD. Integer wrapping_sum reduces lanes directly without calling the scalar module body. Numeric vector locals without mutable borrows stay in vector registers across supported operations and loops. Values materialize in ordinary tagged storage at calls, containers and observable borrows; parameters, masks, generators and mutable-borrowed locals retain that storage path. This is an internal optimization, not a different ownership rule. Checked integer arithmetic, mask summaries, ordered float reductions and list access keep their ordinary scalar implementations. On other compiler hosts, the module’s scalar bodies remain available. The AArch64 implementation currently uses these scalar bodies, including in optimized builds; ARM vector acceleration is separate work. The same source semantics apply with and without optimization. No runtime ISA selection or newer CPU feature is required.

Storage boundaries, copying and lane assembly can dominate short operations. Explicit SIMD is not a blanket speedup over scalar code, and this module does not replace the packed-column lowering used by clist. Wider vectors, packed views, generic lane counts, gathers/scatters, casts, shifts and integer division are not in this initial surface. See the tutorial and the implementation evidence.

Acknowledgements

  • Design follows Rust’s portable std::simd.