SIMD (SSE / NEON / VSX / …)
SIMD (SSE / NEON / …)
go-asmgen does not model vector instructions in its typed surface, and it does
not need to: vector code is emitted through the Raw escape hatch, exactly
as scalar arithmetic is. go-asmgen’s job is to lay out the ABI0 frame and load
the arguments; the packed body is yours to write.
The idiomatic shape is a function that takes pointers to the data, so the ABI0 frame is just pointer slots and the vector loads/stores go through those registers:
// func addI32x4(a, b, out *[4]int32)
sig := arm64.Layout(
[]string{"a", "b", "out"},
[]arm64.Type{arm64.Ptr, arm64.Ptr, arm64.Ptr},
nil, nil,
)
b := arm64.NewFunc("addI32x4", sig, 0)
b.LoadArg("a", "R0").
LoadArg("b", "R1").
LoadArg("out", "R2").
Raw("VLD1 (R0), [V0.S4]"). // NEON: load 4 int32 lanes
Raw("VLD1 (R1), [V1.S4]").
Raw("VADD V0.S4, V1.S4, V2.S4"). // packed add
Raw("VST1 [V2.S4], (R2)"). // store 4 lanes
Ret()The amd64 equivalent is the same structure with SSE2:
MOVQ a+0(FP), AX
MOVQ b+8(FP), BX
MOVQ out+16(FP), CX
MOVOU (AX), X0 // load 4 int32 lanes
MOVOU (BX), X1
PADDL X1, X0 // packed add, dword lanes
MOVOU X0, (CX) // store
RETThe same packed-add example exists for all six targets in
examples/simd,
each runtime-tested: amd64 (SSE2) and arm64 (NEON) natively, and riscv64 (RVV),
loong64 (LSX), ppc64le (VSX) and s390x (vector facility) under qemu with the
vector unit enabled (s390x exercising the big-endian path).
What asmdecl checks here
Only the pointer loads are FP-relative, so asmdecl validates those (three
8-byte pointer reads at offsets 0/8/16). The vector loads/stores go through
registers ((AX), (R0)), which asmdecl does not track — their correctness is
established by the runtime test, which checks every lane.
SIMD across the targets
Every target has runtime-tested packed-add examples:
| Target | extension(s) | key instructions | width |
|---|---|---|---|
| amd64 | SSE2 + AVX2 | MOVOU/PADDL; VMOVDQU/VPADDD | 128 + 256-bit |
| arm64 | NEON | VLD1, VADD.S4, VST1 | 128-bit |
| riscv64 | RVV | VSETVLI, VLE32.V, VADD.VV, VSE32.V | scalable (assembler: Go 1.25) |
| loong64 | LSX + LASX | VMOVQ/VADDW; XVMOVQ/XVADDW | 128 + 256-bit (Go 1.25) |
| ppc64le | VSX | LXVD2X, VADDUWM, STXVD2X | 128-bit |
| s390x | vector facility | VL, VAF, VST (big-endian) | 128-bit |
The amd64 (addI32x8, AVX2) and loong64 (addI32x8, LASX) examples add eight
int32 lanes at once; the others add four.
Go version
The RISC-V Vector and LoongArch SIMD instructions reached the Go assembler in Go 1.25, and the arm64 vector float64 arithmetic in Go 1.27. The module has required Go 1.27 since v0.15.0, and 1.27.1 since v0.15.2.
Why pointers, not by-value vectors
go vet asmdecl rejects a single vector load of a whole by-value array — e.g.
MOVOU a+0(FP), X0 for a [4]int32 argument fails with “invalid MOVOU …;
[4]int32 is 16-byte value”. So passing a vector by value and loading it in
one instruction is not asmdecl-clean. The pointer-based shape above is the
idiomatic Go approach and is what go-asmgen supports. (Fixed-size arrays passed
by value are still usable element-wise — see Aggregates.)
Future work
A typed vector-load helper could emit the pointer load/op/store sequence for you,
removing the Raw boilerplate. See the Roadmap; the Raw path
works today.