Modules

SIMD

moduleextensionSource

Available on macOS, Linux, Windows, Wasm·since 0.31.0

#import "Extensions/SIMD";

Portable fixed-width vectors.

Example↗
SIMD :: #import "Extensions/SIMD";
using SIMD;

text := "hello, wide world!";

// Where is the first space in these 16 bytes?
chunk := load(u8x16, text.data);
spaces := eq(chunk, #char " ");
if any(spaces) print("at %\n", first_true(spaces));

A Vector(T, N) is N lanes of T, at most 64 bytes. Every operation works lane by lane, and its result is the same on every target: the interpreter, x86-64, arm64 and wasm. Compiled with optimization, the operations become the target's vector instructions. Integer arithmetic wraps. Comparisons give a Mask, which selects lanes, reduces to a bool, or becomes a bit per lane.

Module parameters#

VECTOR_BYTES := 16
The vector width code should aim for when it can choose: 16 bytes is SSE2, NEON and wasm simd128, the widest every target of those has. Set 32 when building for AVX2.

Procedures#

splatsince 0.31.0Source
($V: Type, value: V.T) -> V

Every lane set to value.

load3 overloadssince 0.31.0Source
($V: Type, source: *V.T) -> V
($V: Type, items: [] V.T, start := 0) -> V
($V: Type, text: string, start := 0) -> V

V.N values starting at source, which needs no particular alignment.

load_partialsince 0.31.0Source
($V: Type, source: *V.T, count: int, fill: V.T = 0) -> V

The first count lanes from source (0 <= count <= V.N), the rest fill. For the tail of a buffer that has fewer than V.N items left.

store2 overloadssince 0.31.0Source
(vector: Vector($T, $N), dest: *T)
(vector: Vector($T, $N), items: [] T, start := 0)
abssince 0.31.0Source
(a: Vector($T, $N)) -> Vector(T, N)

The absolute value of each lane. The most negative integer stays itself, as it has no positive counterpart.

sqrt3 overloadssince 0.31.0Source
(a: Vector($T, $N)) -> Vector(T, N)
(x: float32) -> float32
(x: float64) -> float64
operator<<since 0.31.0Source
(a: Vector($T, $N), count: int) -> Vector(T, N)

Each lane shifted by count bits, which must be less than the lane's width. >> keeps the sign of signed lanes.

eq2 overloadssince 0.31.0Source
(a: Vector($T, $N), b: Vector(T, N)) -> Mask(T, N)
(a: Vector($T, $N), b: T) -> Mask(T, N)

Comparisons, each giving a Mask. Float lanes compare as scalars do, so NaN is unequal to everything (ne holds, the others don't).

to_bitssince 0.31.0Source
(mask: Mask($T, $N)) -> u64

Bit i set where lane i is set.

($M: Type, bits: u64) -> M

A mask whose lane i is set where bit i of bits is.

last_truesince 0.31.0Source
(mask: Mask($T, $N)) -> int

The highest set lane, or -1 if none is.

shuffle2 overloadssince 0.31.0Source
(a: Vector($T, $N), $order: [] int) -> Vector(T, N)
(a: Vector($T, $N), b: Vector(T, N), $order: [] int) -> Vector(T, N)

Lane i is a[order[i]]. The order is a constant, so this becomes one shuffle instruction.

windowsince 0.31.0Source
(a: Vector($T, $N), b: Vector(T, N), $start: int) -> Vector(T, N)

N lanes of a followed by b, starting at lane start: window(a, b, 0) is a, window(a, b, N) is b. With a the previous block of a stream and b the current one, window(a, b, N - k) is the current block moved k lanes later, with the end of the previous one in front.

lookupsince 0.31.0Source
(table: u8x16, indices: Vector(u8, $N)) -> Vector(u8, N)

A byte table lookup: lane i is table[indices[i]] when that index is below 16, else 0. This is one instruction on SSSE3, NEON and wasm simd128, and the base of fast byte classification: split each byte into nibbles and look both up.

convertsince 0.31.0Source
($V: Type, a: Vector($T, V.N)) -> V

Each lane converted with a cast: floats to integers truncate toward zero, integers to narrower integers keep the low bits.

bitcast2 overloadssince 0.31.0Source
($V: Type, a: Vector($T, $N)) -> V
($V: Type, mask: Mask($T, $N)) -> V

The same bytes seen as another vector type of the same size.

bit_countsince 0.31.0Source
(x: u64) -> int

Bits of a u64, for the masks above.

(x: u64) -> int

The number of 0 bits below the lowest 1 bit; 64 for 0.

(x: u64) -> int

The number of 0 bits above the highest 1 bit; 64 for 0.

(tier: Vector_Tier)

Pretend the processor has nothing above tier: for tests and for comparing versions on one machine. It applies to procedures picked afterwards, so call it before the first call of anything dispatched.

pick_targetsince 0.31.0Source
(baseline: $F, sse42: F = null, avx2: F = null, avx512: F = null, sve: F = null) -> F

The best version the processor can run: the highest tier with a version given, else baseline. A version that needs a feature is compiled with #target_features and listed under its tier.

table = pick_target(count_base, sse42 = count_sse42, avx2 = count_avx2);

Not run: the example is marked ignore.

Types#

Maskstructsince 0.31.0Source
:: struct

One lane per lane of Vector(T, N): all ones where a comparison held, all zeros where it didn't. The lanes are as wide as T, so masks and vectors line up the way the hardware wants.

lanes: [N] #run lane_bits(T)
Cpu_Featuresstructsince 0.31.0Source
:: struct
sse42: bool
x64: SSE4.2 (and everything below it).
avx2: bool
x64: AVX2, and the operating system saves the vector registers it uses.
avx512: bool
x64: AVX-512 F, DQ, BW and VL, and the operating system saves the registers they use.
neon: bool
arm64: NEON, which every arm64 processor has.
sve: bool
arm64: SVE, reported only where it is cheap to ask (Linux).
Vector_Tierenumsince 0.31.0Source
:: enum

The best vector instruction set a processor has, in the order code is picked: each tier has everything below it. On arm64 the baseline is NEON.

BASELINE
SSE42
AVX2
AVX512
SVE