wasm_simd (wasm v0.1.0)

View Source

Fixed-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:

operationbinary128-bit integer
i32x4.add11.8 ns126.0 ns
i8x16.add14.6 ns610.6 ns
f32x4.mul76.9 ns296.7 ns
i32x4.extract_lane5.0 ns17.4 ns
i8x16.shuffle154 ns497 ns
v128.and11.9 ns10.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

Types

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.

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

v128()

-nominal v128() :: binary().

A 128-bit vector: exactly 16 bytes, little-endian lane order.

Functions

binary_op/3

-spec binary_op(atom(), v128(), v128()) -> v128().

Apply a two-operand vector instruction.

bitselect/3

-spec bitselect(v128(), v128(), v128()) -> v128().

Bitwise select: take a bit from A where the mask bit is set.

extract/3

-spec extract(atom(), non_neg_integer(), v128()) -> term().

Read one lane out as a scalar.

is_v128(V)

-spec is_v128(term()) -> boolean().

load/2

-spec load(atom(), binary()) -> v128().

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.

load_lane/4

-spec load_lane(atom(), non_neg_integer(), v128(), binary()) -> v128().

Replace one lane from bytes read out of linear memory.

replace/4

-spec replace(atom(), non_neg_integer(), v128(), term()) -> v128().

Write one lane, returning the whole vector.

shift/3

-spec shift(atom(), v128(), integer()) -> v128().

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.

shuffle/3

-spec shuffle(binary(), v128(), v128()) -> v128().

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.

splat/2

-spec splat(atom(), term()) -> v128().

Broadcast a scalar into every lane.

store_bytes(V)

-spec store_bytes(v128()) -> binary().

The whole vector, ready to be written to linear memory.

store_lane_bytes/3

-spec store_lane_bytes(atom(), non_neg_integer(), v128()) -> binary().

One lane, ready to be written to linear memory.

swizzle(A, B)

-spec swizzle(v128(), v128()) -> v128().

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.

ternary/4

-spec ternary(atom(), v128(), v128(), v128()) -> v128().

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.

unary/2

-spec unary(atom(), v128()) -> v128() | integer().

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().

zero()

-spec zero() -> v128().