Platform-independent SIMD in Go
Go 1.27 adds an experimental platform-agnostic SIMD API
Platform-independent SIMD in Go
Go 1.27 adds an experimental platform-agnostic SIMD API
The Go Blog Platform-independent SIMD in Go David Chase and Junyang Shao 24 September 2026 Go 1.26 and 1.27 include experimental APIs for Single Instruction Multiple Data (SIMD) operations. SIMD is a native feature of many modern CPUs that allows software to perform uniform operations across vectors of data very quickly, such as adding 8 pairs of float64 values in a single instruction. It can significantly speed up many computationally-intensive tasks, ranging from cryptography to data processing to AI. In fact, Go’s Green Tea garbage collector even makes use of SIMD to accelerate scanning memory for live objects. Prior to these new experimental APIs, the only way to access this functionality from Go was by writing Go assembly. This was only worth it for truly performance-critical compute kernels, which meant plenty of software that could benefit from SIMD simply left a lot of the CPU unused. Go 1.26 introduced a SIMD API for amd64, and Go 1.27 added APIs for arm64 (specifically NEON) and wasm. However, a basic challenge for a SIMD API is the enormous variation between platforms, not simply in what operations they support, but even in how vectors are represented. Some platforms provide fixed-size vectors, typically between 128 bits and 512 bits, while on others the vector size isn’t known at build time and must be queried when the program starts. To provide full access to the breadth of these platforms, these APIs live in an architecture-dependent archsimd package. But Go 1.27 goes beyond these architecture-dependent APIs and introduces an experimental, fully portable, platform- and size-agnostic SIMD interface, loosely based on Highway for C++. The goal is to support write-once near-asm-performance “simd” code on platforms with SIMD support, and to provide a competent emulation on those platforms that do not (yet) have SIMD support. The simd package currently supports AVX, AVX2, and AVX512 on amd64, NEON on arm64, and wasm’s SIMD instructions. Motivation: variation among SIMD architectures SIMD architectures vary in several dimensions. Some provide a single fixed vector size (wasm, PowerPC, and s390x, 128 bits). Some provide several fixed vector sizes (amd64, with 128, 256, and 512; loong64 with 128 and 256). Riscv64 supports vectors of unspecified size between 128 and 65536 bits, though the length is limited to powers of 2. Arm64 supports one fixed size (128 bits, NEON), and one variable size (128-2048 bits, powers of two only, SVE). On a given instance of a particular architecture, determining what sizes that particular instance happens to support requires feature checks: amd64, but is it AVX, AVX2, or AVX512? Arm64, but is it NEON or SVE? If SVE, how large? Which variant of SVE: SVE, SVE2, or SVE2.1? Different SIMD architectures vary in how they handle vector masking. For vectors, if-then-else across a vector can be implemented with masks; do the operation, but only assign the result (or load, or store) where the mask is “true”. Some SIMD variants do not provide masks; all operations work across all elements, and “masking” is done with vector bitmasks and vector boolean operations (wasm, AVX, AVX2, NEON). Some provide special mask registers, with one bit governing operations on one vector element (AVX512 and RVV). Others (SVE) allocate one bit per vector byte, but the least-significant bit of each element’s mask bits governs masked operations. AVX2 also supports masked loads and stores, but using a plain vector as the mask, and with the most-significant bit governing the operation. A third source of variation is in the operations themselves. Each architecture provides its own primitives for rearranging vector elements; some require constant inputs, others support variable inputs. Different SIMD architectures support different crypto-related operations. Even basic arithmetic can have varying support; for example wasm lacks comparisons for vectors of 64-bit integers. Even for a given vector length on a particular architecture, instruction support depends on “features” that must be checked. Even though Go’s architecture-dependent archsimd package was designed to be as uniform as possible across architectures, many of these quirks remain, and make designing, writing, and testing code for multiplatform SIMD onerous. We could do more in the archsimd package to make the different architectures appear more similar, but we can only go so far without compromising efficiency. Overview The new simd package hides these differences by removing fixed-size vectors from the type system, and by only supporting those operations that are in the intersection of all the different platforms, and fills gaps in the intersection with efficient emulation in terms of other SIMD instructions. The goal is a set of operations that is adequate to support many data processing algorithms that benefit from a vectorized implementation (but are not tied to a particular vector size), is as efficient as assembly language when the source code operations match the underlying hardware, is otherwise emulated as well as possible, and is easy to read and understand (even/especially if an LLM ends up writing the code). On platforms that lack SIMD instructions or that lack support in archsimd , all of the operations are emulated, so that code written using the simd package will always run. To use this experimental package, set GOEXPERIMENT=simd at build time, just like using the experimental archsimd package. The simd vector types are just capitalized, plural, primitive types, for example simd.Uint8s or simd.Float32s . Vectors are loaded from and stored to slices, for example: // innerProduct returns the inner product of x and y. func innerProduct(x, y []float32) float32 { var a simd.Float32s var i int for i = 0; i < len(x)-a.Len()+1; i += a.Len() { u := simd.LoadFloat32s(x[i : i+a.Len()]) v := simd.LoadFloat32s(y[i : i+a.Len()]) a = u.MulAdd(v, a) } if i < len(x) { u, _ := simd.LoadFloat32sPart(x[i:]) v, _ := simd.LoadFloat32sPart(y[i:]) a = u.MulAdd(v, a) } return sum(a) } // sum returns scalar sum of elements of x. func sum(x simd.Float32s) float32 { s := make([]float32, x.Len()) x.Store(s) var r float32 for _, e := range s { r += e } return r } This example also shows one of the limitations of the first experimental release of this package; because there’s no common way to sum across all the elements of a vector, it’s not supported by simd in Go 1.27, though ReduceSum will appear in the next release so sum can be replaced with just simd.ReduceSum . SIMD comparisons produce mask values, which are specific to the corresponding vector element width, so that comparisons of Int8s produce Mask8s , etc., and mask values can be used to select and filter vectors. Supported simd package operations as of Go 1.27 In this table, V and U are vector types, M is a mask type, E is a scalar type, and W is a width. Package-Level Load / Broadcast Functions Function Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s LoadV([]E) V Y Y Y Y Y Y Y Y Y Y LoadVPart([]E) (V, int) Y Y Y Y Y Y Y Y Y Y BroadcastV(E) V Y Y Y Y Y Y Y Y Y Y Store/String operations (x V).Method(...) Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s Store(s []E) Y Y Y Y Y Y Y Y Y Y StorePart(s []E) int Y Y Y Y Y Y Y Y Y Y String() string Y Y Y Y Y Y Y Y Y Y Arithmetic operations (x V).Method(...) V Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s Abs() V Y Y Y Y Y Add(y V) V Y Y Y Y Y Y Y Y Y Y AddSaturated(y V) V Y Y Y Y Average(y V) V Y Y Div(y V) V Y Y IfElse(mask MaskWs, y V) V Y Y Y Y Y Y Y Y Y Y Len() int Y Y Y Y Y Y Y Y Y Y Masked(mask MaskWs) V Y Y Y Y Y Y Y Y Y Y Max(y V) V Y Y Y Y Y Y Y Y Min(y V) V Y Y Y Y Y Y Y Y Mul(y V) V Y Y Y Y Y Y Y Y MulAdd(y V, z V) V Y Y Neg() V Y Y Y Y Y Y Not() V Y Y Y Y Y Y Y Y Or(y V) V Y Y Y Y Y Y Y Y Sqrt() V Y Y Sub(y V) V Y Y Y Y Y Y Y Y Y Y SubSaturated(y V) V Y Y Y Y Xor(y V) V Y Y Y Y Y Y Y Y Boolean and vector masking operations (x V).Method(...) V Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s And(y V) V Y Y Y Y Y Y Y Y AndNot(y V) V Y Y Y Y Y Y Y Y CarrylessMultiplyEven(y V) V Y CarrylessMultiplyOdd(y V) V Y IfElse(mask MaskWs, y V) V Y Y Y Y Y Y Y Y Y Y Masked(mask MaskWs) V Y Y Y Y Y Y Y Y Y Y Not() V Y Y Y Y Y Y Y Y Or(y V) V Y Y Y Y Y Y Y Y Xor(y V) V Y Y Y Y Y Y Y Y Comparison operations (x V).Method(...) M Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s Equal(y V) MaskWs Y Y Y Y Y Y Y Y Y Y Greater(y V) MaskWs Y Y Y Y Y Y Y Y Y GreaterEqual(y V) MaskWs Y Y Y Y Y Y Y Y Y Less(y V) MaskWs Y Y Y Y Y Y Y Y Y LessEqual(y V) MaskWs Y Y Y Y Y Y Y Y Y NotEqual(y V) MaskWs Y Y Y Y Y Y Y Y Y Y Conversion operations (x V).Method(...) U Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s ConvertToFloatW() FloatWs Y ConvertToIntW() IntWs Y Y Y Y Y ConvertToUintW() UintWs Y Y Y Y ToMask() (to MaskWs) Y Y Y Y Mask Methods (m M).Method(...) M) Mask8s Mask16s Mask32s Mask64s And(y M) M Y Y Y Y Or(y V) V Y Y Y Y String() string Y Y Y Y ToIntWs() (to IntWs) Y Y Y Y Shift and rotate operations (x V).Method() V Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s RotateAllLeft(dist uint64) V Y Y Y Y Y Y RotateAllRight(dist uint64) V Y Y Y Y Y Y ShiftAllLeft(dist uint64) V Y Y Y Y Y Y ShiftAllRight(dist uint64) V Y Y Y Y Y Zero-cost reshaping operations (x V).Method(...) U Int8s Int16s Int32s Int64s Uint8s Uint16s Uint32s Uint64s Float32s Float64s ToBits() UintWs Y Y Y Y Y Y ReshapeToUint8s() Uint8s Y Y Y ReshapeToUint16s() Uint16s Y Y Y ReshapeToUint32s() Uint32s Y Y Y ReshapeToUint64s() Uint64s Y Y Y BitsToFloatW() FloatWs Y Y BitsToIntW() IntWs Y Y Y Y Transition to/from platform-specific code It may happen that the simd package is too limited for all parts of a particular application, or that we have not yet provided an adequate emulation for some necessary feature. For that case, the simd package supports transition to and from architecture-specific SIMD. Each vector type in the simd package has a conversion method ToArch() returning an any . That any can be type-asserted to one of the architecture-specific types for a platform. To convert back, use one of the simd.
Source
Companion reading: Platform-independent SIMD in Go.