wasm_simd (wasm v0.1.0)
View SourceFixed-width SIMD: v128 values and the operations on them. Read this before
you add a vector instruction, or when you want to know what one costs.
Why a 16-byte binary
A v128 is a 16-byte binary. The alternative was a 128-bit integer, and the
choice was measured rather than assumed. Per operation, 200,000 iterations,
minimum of five runs:
| operation | binary | 128-bit integer |
|---|---|---|
i32x4.add | 11.8 ns | 126.0 ns |
i8x16.add | 14.6 ns | 610.6 ns |
f32x4.mul | 76.9 ns | 296.7 ns |
i32x4.extract_lane | 5.0 ns | 17.4 ns |
i8x16.shuffle | 154 ns | 497 ns |
v128.and | 11.9 ns | 10.6 ns |
Bit syntax truncates each field to its declared width, so lane wrapping is free
where the integer form needs an explicit mask per lane, and a binary is built
in one allocation where a shift-and-or chain allocates an intermediate bignum
per lane. The integer form wins only on v128.and, and only by 12%.
Why lanes are matched, not iterated
Every lane operation matches all its lanes in one pattern. The obvious
binary_to_list, lists:zip, comprehension route costs 182 ns for
i8x16.add against 14.6 ns; a binary comprehension with a zip generator is
worse still at 253 ns.
Writing all 240 operations out at full width would be some thousands of lines,
so the lane split is done once per shape in zip16/3 and friends and the
operations are ordinary funs. That costs 1.4x to 1.8x against inlining
(i8x16.add 30.3 ns rather than 17.2 ns) and is still six times better than
iterating. Specialising a hot shape later is a local change.
Floats
Float lanes are not matched with :32/float. Erlang cannot represent NaN or
Infinity as a float, and those bit patterns do not match a float field at all,
so a lane holding a NaN would fail to match rather than compare unequal. Lanes
are extracted as integer bit patterns and converted through wasm_num, giving
the same hybrid representation the scalar instructions use, and so the same
NaN propagation and quieting rules for free.
Summary
Functions
Apply a two-operand vector instruction.
Bitwise select: take a bit from A where the mask bit is set.
Read one lane out as a scalar.
Turn the bytes read from linear memory into a vector.
Replace one lane from bytes read out of linear memory.
Write one lane, returning the whole vector.
Apply a vector shift.
Select 16 bytes from the concatenation of two vectors.
Broadcast a scalar into every lane.
The whole vector, ready to be written to linear memory.
One lane, ready to be written to linear memory.
Select bytes of A by the dynamic indices in B.
Every three-operand vector instruction.
Apply a one-operand vector instruction.
Types
-nominal v128() :: binary().
A 128-bit vector: exactly 16 bytes, little-endian lane order.
Functions
Apply a two-operand vector instruction.
Bitwise select: take a bit from A where the mask bit is set.
-spec extract(atom(), non_neg_integer(), v128()) -> term().
Read one lane out as a scalar.
Turn the bytes read from linear memory into a vector.
The caller does the bounds-checked read; the width it must read is
load_width/1. Splitting it this way keeps every memory concern in
wasm_memory and every lane concern here.
-spec load_lane(atom(), non_neg_integer(), v128(), binary()) -> v128().
Replace one lane from bytes read out of linear memory.
-spec replace(atom(), non_neg_integer(), v128(), term()) -> v128().
Write one lane, returning the whole vector.
Apply a vector shift.
The shift count is taken modulo the lane width, so it is always in range and no lane operation can fault.
Select 16 bytes from the concatenation of two vectors.
The lane indices are immediates, so they are already known to be below 32 by the time this runs: validation rejects anything else.
Broadcast a scalar into every lane.
The whole vector, ready to be written to linear memory.
-spec store_lane_bytes(atom(), non_neg_integer(), v128()) -> binary().
One lane, ready to be written to linear memory.
Select bytes of A by the dynamic indices in B.
Unlike shuffle/3 the indices are runtime values, so an index of 16 or more is
possible and yields zero rather than faulting.
Every three-operand vector instruction.
v128.bitselect was the only one until the relaxed proposal added nine more,
and the interpreter used to match its name as a literal. It dispatches by shape
now, like the one
- and two-operand instructions.
Apply a one-operand vector instruction.
Most return a v128, but bitmask, all_true and any_true return an i32,
which is why the result type is not v128().
-spec zero() -> v128().