SIMD vectors
Vector types for data-parallel math.
On this page
A vector type such as u8x16, i32x4 or f64x2 holds several numbers ("lanes") in one value, and one operation works on all lanes at once. The compiler uses the CPU's SIMD instructions where it can and plain instructions where it can't.
Vector types#
The name is the lane type followed by the lane count: u8x16, i16x8, i32x4, i64x2, f32x4, f64x2, and wider forms such as u8x32, f32x8 and i64x8. A vector is 64 to 512 bits.
Operators work per lane:
+,-and*on every vector,/on float vectors&,|,^and~on integer vectors,<<and>>by a constant count==and!=compare whole vectors and give abool
Make and read vectors#
fn main() {
let values = Array[i32]{ 1, 2, 3, 4 }
let a = $load[i32x4](values, 0) // 4 elements from index 0
let b = $splat[i32x4](10) // every lane 10
let c = a * b + a
println($extract(c, 3)) // 44
println($reduce_add(c)) // 110
let changed = $insert(a, 0, 9)
println($extract(changed, 0)) // 9
}
$load[T](view, offset) reads from an Array, String, &[T] or other view of the lane type, starting at element offset; $store(view, offset, v) writes. Both are bounds checked and need no alignment.
Compare and select#
A compare gives a mask: an integer vector with all bits set in the lanes where it holds.
fn main() {
let a = $load[i32x4](Array[i32]{ 1, 2, 3, 4 }, 0)
let limit = $splat[i32x4](2)
let big = $gt(a, limit)
println($mask_bits(big)) // 12 (lanes 2 and 3)
let capped = $select(big, limit, a)
println($reduce_max(capped)) // 2
}
Example: count bytes#
fn count_spaces(text: local &[u8]) uint {
let spaces = $splat[u8x16](' ')
let total: uint = 0
let i: uint = 0
while i + 16 <= text.length {
let chunk = $load[u8x16](text, i)
total += $count_ones($mask_bits($eq(chunk, spaces)))
i += 16
}
while i < text.length {
if text[i] == ' ' : total++
i++
}
return total
}
fn main() {
println(count_spaces("a b c d e f g h i j k l m n o p q r")) // 17
}
Builtins#
| Builtin | Does |
|---|---|
$splat[T](x) |
Every lane x |
$load[T](view, offset), $store(view, offset, v) |
Read or write a vector in a view |
$extract(v, i), $insert(v, i, x) |
Read or replace one lane |
$shuffle(a, b, i0, i1, ...) |
Pick lanes by constant index: 0.. from a, then from b |
$eq $ne $lt $gt $lte $gte(a, b) |
Per-lane compare, giving a mask |
$select(mask, a, b) |
a where the mask is set, else b |
$mask_bits(mask) |
One bit per lane, as a uint |
$reduce_add, $reduce_min, $reduce_max |
Combine all lanes into one value |
$reduce_and, $reduce_or, $reduce_xor |
Same, for integer vectors |
$bitcast[T](v) |
Reinterpret as another vector type of the same size |
$lookup(table, indexes) |
Byte table lookup, 16 or 32 lanes; needs $target_ssse3 / $target_avx2 on x86-64 |
$clmul(a, b) |
Carry-less multiply of two u64 into a u64x2; x86-64 with $target_pclmul |
Where the type cannot be inferred, it goes in [...].
CPU features#
By default the build targets a baseline CPU. --cpu allows newer instructions for the whole program:
valk build src -o app --release --cpu native # this machine's CPU
valk build src -o app --release --cpu x86-64-v3 # a named CPU level
To ship one executable that still runs everywhere, give a single function a $target_* flag and call it only after core.cpu_has confirms the feature:
use valk.core
fn sum_avx2(values: local &[f32]) f32 $target_avx2 {
let acc = $splat[f32x8](0)
let i: uint = 0
while i + 8 <= values.length {
acc = acc + $load[f32x8](values, i)
i += 8
}
let total = $reduce_add(acc)
while i < values.length {
total += values[i]
i++
}
return total
}
fn sum_plain(values: local &[f32]) f32 {
let total: f32 = 0
each values as v : total += v
return total
}
fn sum(values: local &[f32]) f32 {
if core.cpu_has(.avx2) : return sum_avx2(values)
return sum_plain(values)
}
fn main() {
let values = Array[f32]{ 1.5 x 20 }
println(sum(values)) // 30
}
The flags are $target_ssse3, $target_sse41, $target_avx, $target_avx2 and $target_pclmul (x86-64). Such a function cannot be $inline and cannot take or return a vector wider than 128 bits.
