Skip to content

simd: architecture and vector-size agnostic SIMD intrinsics under a GOEXPERIMENT #78902

Description

@dr2chase

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

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposalProposal-Accepted

    Type

    No type

    Projects

    Status
    Accepted

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions