SIMD, arrays, and pointers
API summary (generated from the Beans source by npm run coverage): 7 types · 6 static methods · 32 instance methods.
This page covers the low-level builtins: SIMD vectors, fixed-size arrays, slices, and raw pointers. These are for tight, hardware-close code.
SIMD vectors
Section titled “SIMD vectors”SIMD (“single instruction, multiple data”) does the same math on several numbers at once. Each SIMD type packs a fixed number of equal-typed numbers, called lanes.
A SIMD type is named Simd + lane count + element type, for example:
Simd4i32: 4 lanes ofi32Simd16u8: 16 lanes ofu8Simd2f64: 2 lanes off64Simd4f32: 4 lanes off32
Element types are i8/i16/i32/i64, their u forms, and f32/f64. The
lane count is a power of two. Total width is 128 bits everywhere. A 256-bit width
needs CPU features, for example Simd8i32 needs --features +avx2.
Making a vector
Section titled “Making a vector”| Constructor | Notes |
|---|---|
splat(x) | every lane set to x |
of(...) | one value per lane |
load(ptr) | read lanes from memory |
load_unaligned(ptr) | read from memory that may not be aligned |
| Method | Returns | Notes |
|---|---|---|
lane(i) | element | read lane i |
with_lane(i, v) | vector | a copy with lane i set to v |
lane_count() | int | how many lanes |
Arithmetic works lane by lane. You can use the operators + - * / or the named
methods add, sub, mul, div, plus min and max.
Integer families also have bit_and, bit_or, bit_xor, bit_not, shl, and
shr.
Comparisons and masks
Section titled “Comparisons and masks”The comparisons eq, ne, lt, le, gt, ge return a mask (a per-lane
true/false). With a mask you can:
mask.select(a, b): pick fromawhere true, elsebmask.any_true(): is any lane truemask.all_true(): are all lanes true
Reductions and storing
Section titled “Reductions and storing”| Method | Notes |
|---|---|
sum() | add all lanes |
product() | multiply all lanes |
store(ptr) | write lanes to memory |
store_unaligned(ptr) | write to memory that may not be aligned |
A SIMD value cannot be a Map key: it has no Hash.
Fixed arrays: [T; N]
Section titled “Fixed arrays: [T; N]”[T; N] is a fixed inline array of N items of type T, where 1 <= N <= 4096.
Unlike a List, it is not a handle: it copies by value.
N is an integer literal — decimal, hex, binary, digit separators and all — or a
module constant that folds to an integer in that
range, so const LIMIT: int = 128 lets you write [int; LIMIT]. A const cannot be a parameter default, which is
read at a different stage.
- Indexing is checked (panics if out of range).
- You can assign an element when the binding is
var. array.len() -> intgivesN.array.get(int) -> Option<T>returns a copyable element when present andnonefor a negative or out-of-range index.- Two arrays compare equal with
==. - You can loop over one with
for.
A stable for loop reads the inline array directly when its item binding cannot
escape. If the loop can change the array, the compiler uses a snapshot.
A list literal takes on fixed-array meaning from the declared type:
var lanes: [f32; 4] = [1, 2, 3, 4]lanes[0] = 9let n: int = lanes.len()Slice<T>
Section titled “Slice<T>”Slice<T> is a non-owning view: a pointer plus a length. It does not own the data
it points at. All of its operations require unsafe and are bounds-checked.
Slice.from_raw(ptr, len)
Slice<T>.get(int) -> Option<T>Slice<T>.set(int, T)Slice<T>.subslice(int, int) -> Slice<T>Slice<T>.as_ptr() -> RawPtr<T>Slice<T>.len() -> intSlice.from_raw(ptr, len)makes a slice overlenitems atptr. A non-empty slice rejects a null pointer.get(i)returnssome(item)when present andnonefor a negative or out-of-range index.s[i]requires the element and panics out of range.set(i, v)ands[i] = vwrite an existing item and panic out of range.getstill requiresunsafe: an in-range index cannot prove that the allocation is alive. To migrate a required read from 0.1.53, uses[i].subslice(from, to)is a smaller view over the same storage.as_ptr()hands back the underlying pointer;len()is the number of items.- You can loop over a slice with
for.
unsafe { let view: Slice<i32> = Slice.from_raw(ptr, 4) let first: i32 = view[0]}RawPtr<T>
Section titled “RawPtr<T>”RawPtr<T> is a raw pointer to memory of type T. Every use is inside an
unsafe {} block. This is a brief list; see the unsafe guide for
the full detail and the rules you must follow.
Statics make or name a pointer:
RawPtr.alloc(count)RawPtr.alloc_aligned(count, align)RawPtr.null()RawPtr.from_address(u64)RawPtr.with_local(inout local, fn(RawPtr<T>))alloc(count)allocates room forcountitems;alloc_aligned(count, align)does the same with a chosen alignment.null()is a null pointer;from_address(u64)is a pointer at a raw address.with_local(inout local, fn(RawPtr<T>))runs your function with a raw pointer to a stack local. The pointer is valid only for that call.
The instance methods:
RawPtr<T>.read() -> TRawPtr<T>.write(T)RawPtr<T>.read_volatile() -> TRawPtr<T>.write_volatile(T)RawPtr<T>.offset(int) -> RawPtr<T>RawPtr<T>.address() -> u64RawPtr<T>.is_null() -> boolRawPtr<T>.element_size() -> intRawPtr<T>.element_align() -> intRawPtr<T>.copy_from(RawPtr<T>, int)RawPtr<T>.fill_zero(int)RawPtr<T>.free()RawPtr<T>.atomic_load() -> TRawPtr<T>.atomic_store(T)RawPtr<T>.atomic_fetch_add(T) -> TRawPtr<T>.atomic_compare_exchange(T, T) -> boolread/writemove a value in and out; the_volatilepair does the same without letting the compiler reorder or drop the access.offset(n)movesnitems along;address()is the raw address andis_null()tells you whether the pointer is null.element_size()andelement_align()are the size and alignment ofT.copy_from(src, n)copiesnitems fromsrc;fill_zero(n)zerosnitems;free()releases memory that came fromalloc.- The
atomic_*methods do sequentially consistent atomic access through the pointer.
unsafe { let p: RawPtr<i32> = RawPtr.alloc(4) p.write(7) let x: i32 = p.read() p.free()}RawSlice
Section titled “RawSlice”RawSlice is the untyped, two-word {pointer, length} value that backs a
Slice<T>. Slice<T> is the typed view you normally use; RawSlice is the
element-agnostic form the compiler and low-level code use for the same
pointer-plus-length shape. Like the other raw types it is only meaningful inside
unsafe, and size_of(RawSlice) folds to two pointers for the selected target.
Prefer Slice<T> in your own code, it carries the element type and
bounds-checks its accesses.
A worked example
Section titled “A worked example”Four f32 lanes multiplied and added, then stored through a RawPtr and read
back. Everything low-level here sits inside unsafe:
import std.io
fn main() { unsafe { let source: Simd4f32 = Simd4f32.of(1.0, 2.0, 3.0, 4.0) let scale: Simd4f32 = Simd4f32.splat(2.0) let result: Simd4f32 = source * scale + source io.println("lane0 {result.lane(0)} sum {result.sum()}")
let memory: RawPtr<f32> = RawPtr.alloc(4) result.store(memory) let view: Slice<f32> = Slice.from_raw(memory, 4) io.println("view len {view.len()} first {view[0]}") memory.free() }}See also
Section titled “See also”- The unsafe guide, the full model and every
RawPtrrule. - FFI, calling C code.
- Atomics, the safe
Atomic<T>type.