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).
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
Int8sorFloat64s. Within a given execution of a program all the vectors have the same length in bits, so they may be punned withAs<OtherTypeSize>s, for examplex.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, currentlyAndandOr. On architectures/variants supporting single operation+mask instructions, it is intended thatx.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() anyandToSimd() anyfollowed by a type case that includes the desired types.For example:
There is a candidate CL; the actual API appears in
simd/mocks.go. It is generated automatically bysimd/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
and
Float32ssupports these functions and methodsImplementation 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).