Repository navigation
simd: architecture and vector-size agnostic SIMD intrinsics under a GOEXPERIMENT #78902
Description
Proposal Details
This is the first version of the "second level" SIMD api described in #73787; the intent is to provide a portable interface to SIMD vectors that is not tied to a particular architecture or vector length. On architectures where the instructions are supported, the API provides high performance, on those where it is not, it at least provides portability and a competent emulation. The API will also automatically pick the "best" (largest vector length, latest technology) when run on different variants of a different architecture (that is, it will auto-select between AVX, AVX2, and AVX512, depending on what is available).
In this version, the supported vector methods are those in the intersection of the wasm SIMD API and the current amd64 SIMD API; this is a small but still useful set of operations. Starting small also simplifies the emulation for those platforms that do not yet support SIMD, and also increases the chance that newly implemented SIMD support for some other architecture will easily be compatible.
API overview
The vector type names are all the signed, unsigned, and float types, with first letter capitalized, and a plural suffix, for example Int8s or Float64s. Within a given execution of a program all the vectors have the same length in bits, so they may be punned with As<OtherTypeSize>s, for example x.AsFloat64s().
Masks are supported as an abstraction (because all the architectures implement them differently), with a bit width to indicate the size of compatible vector elements, and all non-mask types support the corresponding .Masked(mask) and .Merged(valueIfFalse, mask) methods. Masks can also be manipulated with simple booleans, currently And and Or. On architectures/variants supporting single operation+mask instructions, it is intended that x.Operation(y).Masked(m) pattern-match into that single instruction.
It is possible to convert an architecture-agnostic simd type into a specific architecture's simd type, and back again. This isn't in the prototype yet, the proposed methods names for this are ToArch() any and ToSimd() any followed by a type case that includes the desired types.
For example:
//go:build amd64
...
import (
"simd"
"simd/archsimd"
)
func sum(x simd.Float32s) float32 {
switch a := x.ToArch().(type) {
case archsimd.Float32x8:
a = a.AddPairsGrouped(a)
a = a.AddPairsGrouped(a)
return a.GetLo().GetElem(0) + a.GetHi().GetElem(0)
case archsimd.Float32x16:
...
case archsimd.Float32x4:
...
}
There is a candidate CL; the actual API appears in simd/mocks.go. It is generated automatically by simd/archsimd/_gen/midway/ from an intersection of the SIMD APIs for wasm and amd64 (and other architectures, in the future). The portable API will be expanded over time by various architecture-specific APIs with emulations to fill in the intersection.
As an example, "Int8s" supports the following functions and methods
LoadInt8Slice, LoadInt8SlicePart, Abs, Add, AddSaturated, And, AndNot,
AsFloat32s, AsFloat64s, AsInt16s, AsInt32s, AsInt64s, AsUint16s, AsUint32s,
AsUint64s, AsUint8s, Equal, Greater, Len, Masked, Max, Merge, Min, Neg, Not,
Or, StoreSlice, StoreSlicePart, String, Sub, SubSaturated, ToMask, Xor
and Float32s supports these functions and methods
LoadFloat32Slice, LoadFloat32SlicePart, Add, AsFloat64s, AsInt16s, AsInt32s,
AsInt64s, AsInt8s, AsUint16s, AsUint32s, AsUint64s, AsUint8s, ConvertToInt32,
Div, Equal, Greater, GreaterEqual, Len, Less, LessEqual, Masked, Max, Merge,
Min, Mul, MulAdd, NotEqual, Sqrt, StoreSlice, StoreSlicePart, String, Sub,
Implementation technique
Code that depends on "simd" is rewritten into multiple versions, and functions and methods that internally depend on simd but do not export that dependence in their signatures will be turned into dispatch functions/methods. Within code compiled for a particular variant, references to other simd-dependent code will be rewritten to reference the same variant; the intent is to both avoid internal dispatch and minimize code cloning. This rewrite also occurs early in the compiler to maximize the chance of inlining (for performance, of course). The rewriting generates names with "@" characters in them, so they cannot be referred to from source code (this may have some effect on tools processing export data or symbols in object files, e.g., debuggers).
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsAccepted
Activity
ianlancetaylor commentedon Apr 22, 2026
Since the length is not known when writing the program, I think it would help to show a couple of examples of how to write code that actually uses these new types and functions/methods. Thanks.
balasanjay commentedon Apr 22, 2026
(copying over my comment from dr2chase/midway#1 and closing that)
Let's say I wanted to write an algorithm specifically using simd.Uint32x8 (for cryptographic reasons, or hash-table sizing reasons (e.g. I'm storing 8 uint32 hash codes in a single vector)).
I would want that to compile to archsimd.Uint32x8 natively for platforms supporting 256-bit operations, but I would want it to also generate an implementation using a pair of archsimd.Uint32x4 values representing lo and hi elements in my logical 8-vector.
(Actually, for that matter, I may even want it to compile it into a variant that is implemented using 8 uint32s that work without any archsimd support at all).
Is the intention for midway/simd package to support use-cases like that (fixed lane size but generic over underlying hardware lane size), or is the use-case limited to code that is "generic" over the number of lanes.
I believe highway supports this via FixedTag<T, kCount> (and what is currently implemented is equivalent to ScalableTag).
dr2chase commentedon Apr 22, 2026
@ianlancetaylor
The canonical example is inner product:
"Sum" is implemented two different ways, chosen by build tag:
or
dr2chase commentedon Apr 22, 2026
@balasanjay The use case is code that is not sensitive to the vector width. You can however inquire about the length of a
simd.Uint32sand chose different code based on that, and also use build tags (in the case ofsumabove) to specialize by architecture. The one thing that won't work is convertingarchsimd.Uint32x8directly intosimd.Uint32sif you happen to be running on AVX512, since in that casesimd.Uint32sis actuallyarchsimd.Uint32x16.This gets more annoying given the scalable SIMD architectures (SVE, RVV) that in theory and sometimes in practice can have more than 256 bits -- code generation to 256-bit vector operations in that case potentially requires a mask applied to all otherwise-larger operations. (Last I checked, real-world SVE was 128, 256, and a 512-bit Japanese supercomputer. There are several RISCV implementations with 512-bit RVV.)
balasanjay commentedon Apr 24, 2026
@dr2chase Hmm, I see.
I understand the usecase for
simd.Uint32s. As you say, it allows you to write portable code that is not sensitive to the vector width. It will compile variations for architectures witharchsimd.Uint32x4support,archsimd.Uint32x8support, etc.I think there is also a usecase for
simd.Uint32x8. It would allow you to write portable code for dealing with specifically 8 uint32s. It would of course compile a variation for an architecture witharchsimd.Uint32x8support (trivially), but it would also compile a variation for an architecture with justarchsimd.Uint32x4support (by emulating asimd.Uint32x8as twoarchsimd.Uint32x4). It would even compile a variation for a scalar architecture (by emulating asimd.Uint32x8as eightuint32s).Consider the program I wrote to test out
archsimdmany months ago: link. Note: this predated the package rename, sosimdin that code should be read asarchsimd(confusing, I admit). That function is written usingarchsimd.Uint8x32because that expresses the computation required by the spec for this encryption algorithm, but I don't want to write separate AVX512, AVX2, SSE, and scalar version of that algorithm. I think using build tags and/or inquiring about the length of asimd.Uint32sare strategies for me writing all those variants. I'd like the compiler to just generate those variants for me. Maybe you're saying that that is out-of-scope for the initial version of the portable simd package.(ignoring mildly tricky AES situation later in that file)
That file does have 1 tricky aspect which really does seem to require AVX: [the Wide AES](https://github.com/balasanjay/aegis/blob/ae41f298cff5b5a615127c11ab2ac4979384466d/internal/impl/aegis128x2.go#L247). But even that could be emulated with the non-vector AES instructions. Or in the worst case, with a fallback to a software AES implementation. I could buy that a vector AES function specifically should be left out of the portable `simd` package and put elsewhere. But certainly all the simple functions (`Xor`, `And`, `LoadFromSlice`, etc) should be available in the portable package, without resorting to writing `archsimd` code directly.aclements commentedon Apr 29, 2026
@balasanjay , thanks for the example. Certainly for now this is out of scope. In particular, emulating fixed widths that are wider than the hardware supports would be quite a lot of work (doable, certainly, but not something we're going to tackle at first).
aclements commentedon Apr 29, 2026
Pasting our discussion on possible approaches for going between
simdandarchsimd:aclements commentedon Apr 29, 2026
To clarify @dr2chase 's
sumexamples, these show a reducing sum operations, not a vector-wise sum. The current prototype doesn't directly support any reduction operations because no hardware directly supports this. Reduction operations are useful enough we would probably want to add them, and we'd do that by adding emulations toarchsimdso they wind up in the intersection of operations exposed in thesimdpackage.aclements commentedon Apr 29, 2026
@dr2chase , could you post a complete API? It can be a
go docor a pointer to a CL if there's already a good summary somewhere (maybemocks.gois that?)dr2chase commentedon Apr 30, 2026
Here is most of the API, at today's version: https://go.dev/cl/c/go/+/763100/16/src/simd/mocks.go
This is automatically generated from existing architecture-specific APIS. There may be some operations that we wish were there, and if so, say so, but the proposed mechanism for obtaining more operations is to add emulations to the architecture-specific APIs, which then increases the size of their intersection.
The missing part of the API is the conversions between simd and archsimd. I have a minor preference for a combination of option 1 and 2b; I haven't completely explored the FromArch method/function, but adapted the "sum" code to use the two different options. The answer to
is "panic", if we don't like panic then it needs to return
(Int8s, bool)and it might be slower, too.As noted above,
sumexists because we don't have aReduceAddmethod in the existing archsimd APIs, so it is also not in their intersection. I wrote it like this because it was an easy example of dropping from architecture-agnostic down to architecture-specific code, and I could compile it and run it and compare its behavior. All the type-wrangling vanishes after optimization; because the code is specifically recompiled for each particular vector width, all the type predicates become constants.Option 1:
Option 2b (which required enabling generic methods and then provoked a crash in type-checking, so not currently my favorite):
dr2chase commentedon May 6, 2026
I took a stab at rearranging the proposal into a more digestible form (yes an LLM was involved, I had to proofread carefully but the formatting is nice and the organization looks helpful).
Experimental size-and-architecture agnostic SIMD Reference
This document provides a comprehensive functional summary of all type declarations and SIMD operations currently defined in the proposed "simd" package, which is mostly the contents of mocks.go . Some adjustments are underway to align this with Austin's recent proposed changes to the amd64 SIMD API; the goal is to track that.
The abstract API in
mocks.gorepresents the computed intersection of SIMD methods supported across all target SIMD architectures (such asamd64andwasm) and vector widths. The current version is missing a few methods that can be supplied with straightforward emulation, and these are noted, but may not be complete by the 1.27 freeze.Vector & Mask Types Reference
The API defines specific vector types and corresponding boolean mask types, categorized by element width and representation:
Int8sMask8sInt16sMask16sInt32sMask32sInt64sMask64sUint8sMask8sUint16sMask16sUint32sMask32sUint64sMask64sFloat32sMask32sFloat64sMask64s1. Global & Utility Operations
Global Capacity Queries
VectorSize() intReturns the size (in bits) of the SIMD vectors supported by the current host CPU for this execution.Intrinsic Utilities
x.Len() intReturns the number of elements in the vector (available on all vector types).x.String() stringReturns a string representation of the vector or mask (available on all vector and mask types).2. Memory Operations (Loads & Stores)
These operations load data from standard slices into SIMD registers or store SIMD register values back to memory slices.
Slice Loads
Load<Type>Slice(s []elem) <Type>Loads a full slice into a vector register.LoadInt8Slice([]int8) Int8sLoad<Type>SlicePart(s []elem) <Type>Loads a partial slice into a vector register (e.g., when slice length is less than the register width).LoadInt8SlicePart([]int8) Int8sLoadMask<Size>Slice(s []elem) Mask<Size>sLoads slice element values into a boolean mask vector.LoadMask8Slice([]int8) Mask8sLoadMask<Size>SlicePart(s []elem) Mask<Size>sLoads a partial slice element values into a boolean mask vector.LoadMask8SlicePart([]int8) Mask8sVector Stores
x.StoreSlice(s []elem)Stores vector elements back into the destination slices.(x Int8s) StoreSlice(s []int8)x.StoreSlicePart(s []elem)Stores a partial vector back into the destination slices.(x Int8s) StoreSlicePart(s []int8)[!NOTE] Boolean mask types (
Mask8s,Mask16s,Mask32s,Mask64s) support load operations, but do not support directStoreSlice*operations. To store a mask, it must first be converted to an integer vector (see Casts & Conversions).3. Arithmetic Operations
Basic Arithmetic
x.Add(y <Type>) <Type>Returns the element-wise sum (x.Sub(y <Type>) <Type>Returns the element-wise difference (x.Mul(y <Type>) <Type>Returns the element-wise product (Int16s,Int32s,Uint16s,Uint32s,Float32s,Float64s.Int8s,Uint8s,Int64s,Uint64s(currently).x.Div(y <Type>) <Type>Returns the element-wise quotient (Float32s,Float64s.x.MulAdd(y <Type>, z <Type>) <Type>Fused Multiply-Add: ReturnsFloat32s,Float64s.Unary & Sign Arithmetic
x.Neg() <Type>Returns the element-wise negation of x.Int8s,Int16s,Int32s,Int64s)Float32s,Float64sviaSubfrom zero).x.Abs() <Type>Returns the element-wise absolute value.Int8s,Int16s,Int32s.Int64s.x.Sqrt() <Type>Returns the element-wise square root.Float32s,Float64s.Saturated Arithmetic
Saturated operations clamp values to the minimum/maximum values representing the integer width instead of wrapping around.
x.AddSaturated(y <Type>) <Type>Returns saturated sum ofInt8s,Int16s,Uint8s,Uint16s).x.SubSaturated(y <Type>) <Type>Returns saturated difference ofInt8s,Int16s,Uint8s,Uint16s).Reduction & Blending Arithmetic
x.Min(y <Type>) <Type>Returns the element-wise minimum ofInt64s,Uint64s).x.Max(y <Type>) <Type>Returns the element-wise maximum ofInt64s,Uint64s).x.Average(y <Type>) <Type>Returns the element-wise average ofUint8s,Uint16s.4. Bitwise & Logical Operations
Bitwise operations are performed on a bit-by-bit level across entire vector registers.
x.And(y <Type>) <Type>Bitwise AND (Int8s,Int16s,Int32s,Int64s,Uint8s,Uint16s,Uint32s,Uint64s) and mask types (Mask8s,Mask16s,Mask32s,Mask64s).x.Or(y <Type>) <Type>Bitwise OR (x.Xor(y <Type>) <Type>Bitwise XOR (x.AndNot(y <Type>) <Type>Bitwise AND-NOT (x.Not() <Type>Bitwise NOT (5. Shift Operations
Shift operations shift all elements inside the vector left or right by a uniform bit count specified as a
uint64.x.ShiftAllLeft(y uint64) <Type>Shifts all elements left byInt16s,Int32s,Int64s,Uint16s,Uint32s,Uint64s.Int8s,Uint8s).x.ShiftAllRight(y uint64) <Type>Shifts all elements right byInt16s,Int32s,Uint16s,Uint32s,Uint64s.Int8s,Uint8s) and 64-bit signed integers (Int64s).6. Comparisons & Masking
Comparison operations evaluate conditions element-wise, returning boolean mask vectors of matching layouts.
Comparisons
x.Equal(y <Type>) <Mask>Returns a mask indicating where elements are equal (x.Greater(y <Type>) <Mask>Returns a mask indicating whereInt8s,Int16s,Int32s,Int64s) and floating-point types (Float32s,Float64s).x.GreaterEqual(y <Type>) <Mask>Returns a mask indicating whereFloat32s,Float64s.x.Less(y <Type>) <Mask>Returns a mask indicating whereFloat32s,Float64s.x.LessEqual(y <Type>) <Mask>Returns a mask indicating whereFloat32s,Float64s.x.NotEqual(y <Type>) <Mask>Returns a mask indicating whereFloat32s,Float64s.Element Blending & Selection (Masking)
x.Masked(mask <Mask>) <Type>Element-wise masking: returns a new vector containing elements frommaskis true, and zero elsewhere.x.IfElse(mask <Mask>, y <Type>) <Type>Element-wise blending: returns a new vector containing elements frommaskis false, and elements frommaskis true.7. Casts & Conversions
Casts and conversions transform vector layouts either by bitwise reinterpretation or by numerical conversion of elements.
Bitwise Reinterpretation (No-op Casts)
To avoid a large matrix of methods, bitwise reinterpretation is factored into puns to same-sized Uint vectors, puns from same-sized Uint vectors, and size conversions of Uint vectors. This API would have 90 reinterpretation methods if it were unfactored, but on AMD64 where there are 3 fixed wvector widths, there would be 270 reinterpretation methods. The names here may change based on updates to the AMD64 SIMD API; whatever is chosen, these will agree, but the division into 3 kinds of reinterpration is not expected to change.
x.ToBits() Uint<W>sReturns the bits ofUintsof the same widthSupported by: All signed integer types (
Int8s,Int16s,Int32s,Int64s) and floating-point types (Float32s,Float64s).x.BitsToFloats() Float<W>sReturns the bits ofUint<W>sx reinterpreted as aFloat<W>s.x.BitsToInts() Ints<W>sReturns the bits ofUint<W>sx reinterpreted as anInt<W>s.x.ReshapeToUint8s()x.ReshapeToUint16s()x.ReshapeToUint32s()x,ReshapeToUint64s()Value Conversions (Element-wise Value Casts)
Numerically converts element values from one primitive representation to another. Expanding this portion of the API is desirable but it will require emulation work on various architectures because there is great variety in how expanding and contracting vector elemens are handled.
(x Int32s) ConvertToFloat32() Float32sConverts integer elements to float32 values.(x Float32s) ConvertToInt32() Int32sConverts float32 elements to int32 values.Mask Conversions
Note: signedness of the int types that interconvert with masks is TBD in the amd64 SIMD spec, and this will follow that if it changes to unsigned.
x.ToMask() <Mask>Converts a signed integer vector x to its boolean mask equivalent.Int8s(Mask8s),Int16s(Mask16s),Int32s(Mask32s),Int64s(Mask64s).m.To<IntegerType>() <IntegerType>Converts a mask m back to its signed integer vector equivalent (with all bits set to 1 for true, 0 for false).(x Mask8s) ToInt8s() Int8s8. Translation to/from architecture specific types
x.ToArch() anyConverts x, an architecture agnostic SIMD vector, into ananythat can be type-asserted or type-switched into the platform-specificsimd/archsimdvector type that implements x.<SimdType>FromArch[T archSimd<SimdType>](x T) <SimdType>Converts x, an architecture-specific SIMD value, into<SimdType>, an architecture-agnostic SIMD vector. The exact definition ofarchSimd<SimdType>varies across architectures, as an example, for Float32 vectors on amd64 it isand
aclements commentedon May 6, 2026
Proposal review discussion:
<SimdType>FromArch[T archSimd<SimdType>](x T) <SimdType>- Does thispanicifTisn't the expected lane count?Int32salways the same length in a given run of a program? Yes.Load<Type>SlicePart(s []elem) <Type>- We're a little concerned that this doesn't return the actual number of elements loaded. Yes, you can get that frommin(len(s), x.Len()), but that's a bit cumbersome and it would be easy to accidentally just usex.Len()in a loop.unsafe.Sizeof(typeParam)is a non-constant). Part of the reason we want to do this as an experiment for a release is to iron out bad tools interactions.43 remaining items
internal/cpu, runtime: detect ARM64 SVE and vector length
runtime: save ARM64 SVE state during async preemption
runtime: dynamically size the async-preemption extended register save…
internal/cpu: detect RISC-V vector extension and VLEN at startup
runtime: save RISC-V vector registers state during async preemption
internal/cpu, runtime: detect ARM64 SVE and vector length
runtime: save ARM64 SVE state during async preemption
internal/cpu: detect RISC-V VLENB when the vector extension is available
runtime: dynamically size the async-preemption extended register save…
runtime: save RISC-V vector registers state during async preemption
internal/cpu, runtime: detect ARM64 SVE and vector length
runtime: remove the SVE vector length limit in async preemption
gopherbot commentedon Aug 21, 2026
Change https://go.dev/cl/819140 mentions this issue:
simd, simd/archsimd: correct package documentationinternal/cpu: detect ARM64 SVE and vector length
simd, simd/archsimd: correct package documentation