The Go SIMD code generators
This directory contains the code generators for Go’s SIMD support, along with
the libraries and debugging tools they are built from. It is a separate module
from the main Go tree. main.go is the driver that runs all the generators, or
they can be run individually.
We generate for four targets: amd64, arm64 (NEON), arm64 SVE, and wasm. SVE is not a GOARCH — it is an arm64 feature with its own generated files, its own scalable vector types, and its own simdgen invocation. It behaves like a separate target throughout, so “four targets” always means these four.
The packages we generate
The Go SIMD API lives under $GOROOT/src/simd:
| Path | What it is |
|---|---|
simd/ |
Portable package. Length-agnostic vector types only (Int8s, Float32s, Mask8s, …). Works on every platform. |
simd/archsimd/ |
Platform package. Fixed-width types (Int8x16 … Float64x8) plus scalable types on SVE. Contents vary by GOARCH. |
simd/internal/spec/ |
The executable specification. Buildable Go, but not meant to be called directly. |
simd/internal/simdref/ |
Reference implementation of the API, generated from spec by refgen. |
simd/internal/bridge/ |
Glue between simd and archsimd. Generated by midway, not user-visible. |
The “one meaning” rule
Both simd and archsimd follow a single rule, which is the hardest-won
property of the whole project:
One name means one thing. A name need not exist on every platform or in both packages, but wherever it does exist it must denote the same operation, with the same signature and the same semantics.
A method must mean one thing across all types within a category, where the
only two categories are vectors and masks. Furthermore, its signature should
vary only in minor ways that depend on the receiver (e.g., it may be sensitive
to the number of lanes). simd and archsimd even have exactly the same
scalable vector types. On fixed-width architectures, the types differ, but
method names, signatures, and meanings still match.
Method names shared between categories should at least be logically related.
For example, Mask8x16.ToBits() yields a uint16 bitmap while
Float32x4.ToBits() reinterprets the bits as Uint32x4. Different shapes,
plainly related meaning, both correct.
One caveat is that implementations of a method may differ in minor, hardware-dependent ways, such as floating point rounding and treatment of out-of-range conversions. The allowed wiggle room is not yet well-defined.
Historically the rule was maintained by
apisum,
which audits after the fact. spec is the same idea in reverse — consistent by
construction rather than by observation — and it doubles as documentation and a
behavioral oracle.
The intersection corollary
The simd package API is the intersection of the archsimd package APIs across
all architectures, after generalizing fixed-width to scalable width, plus a few
methods for querying emulation behavior. This is only possible because of the
“one meaning” rule.
Another consequence of this: if we want to add an operation to simd, we do so
by ensuring it’s implemented in archsimd across all architectures, clearly
documenting where it’s an emulation in the archsimd package.
The key constraint of the simd surface is that every operation must be either
supported natively or efficient to emulate on every architecture. In general, we
follow the Highway project’s lead on what operations can be universally
efficient.
The generators
| Directory | Role |
|---|---|
simdgen/ |
The big one. Unification-based. Produces the amd64/arm64/SVE API and the compiler backend. |
tmplgen/ |
Template expansion for slice ops, compares, mask merges, conversions, and testing boilerplate. |
wasmgen/ |
The wasm API. |
midway/ |
Computes the intersection of methods across architectures to produce the portable simd package’s declarations. |
specgen/ |
Interprets internal/spec into a concrete API description. |
specgen/specexpr/ |
Constraint solver over vector shapes. |
unify/ |
The unification engine simdgen is built on. |
gentools/ |
General shared file-writing utility: -w, -diff, -txtar, -goroot. Used by every generator. |
sgutil/ |
Assorted helpers specific to these generators: path resolution, ordered maps, natural sort. |
cmd/refgen/ |
Generates internal/simdref from spec. |
cmd/specls/ |
Not a generator: prints the API that spec expands to. The main debugging tool for spec. |
The best way to see what files in the simd packages are generated and by what
tools is grep -r '^// Code generated by'. In addition, simdgen generates the
compiler’s ssa/_gen/simd*ops.go, simd*.rules, and
ssagen/simd*intrinsics.go files.
How simdgen works
simdgen unifies four inputs, decodes the unified results into Operation
values, performs several canonicalization and update steps, then runs several
emitters over the final set of Operations.
| Input | Content |
|---|---|
Platform loader (xed.go, arm64/, sve/) |
Machine instructions parsed from ISA tables into simdgen defs |
types.yaml |
Vocabulary of legal types as simdgen defs |
ops/*/categories.yaml |
Platform-independent API defs: name, doc, commutativity |
ops/*/go_GOARCH.yaml |
Glue between the ISA defs and Go API: go: (often a regexp), asm: pattern, operand patterns, and a long tail of quirk fields |
Two structural facts that are easy to miss:
-
!import ops/*/categories.yamlproduces a single global sum. Theops/AddSub/,ops/Moves/directory structure is cosmetic. Nothing scopes a directory’s category entries to that directory’s arch entries; everything cross-joins with everything. -
unify.Deftreats any absent field asTop, so there is no schema. Any invented or misspelled key is silently accepted and silently inert. Several keys in the checked-in YAML are dead weight for exactly this reason.
The external instruction data is not checked in. Use fetch-xed.sh and
fetch-arm64.sh, or point -xedPath / -arm64Path (or $XEDPATH /
$ARM64_ISA_PATH) at existing copies.
What spec and specgen are
internal/spec describes operations as generic Go functions:
// Add adds corresponding elements of two vectors.
//
// z[i] = x[i] + y[i]
func Add[E Nums, W Width](x, y Vec[E, W]) (z Vec[E, W]) {
return map2[E, W, E, W](x, y, func(x, y E) E { return x + y })
}
specgen.Load(dir string, opts *LoadOptions) returns ([]*specgen.Func, error). Each Func is a fully instantiated, concrete API signature.
Recv, In, and Out are specgen.Arg values — a parameter name plus a
specexpr.Type — so a Func carries argument names as well as types, which
matters to anything comparing signatures. There is also Name and Doc. Read
simd/internal/spec/doc.go for the //specgen:name, //specgen:require, and
{{.var}} doc-template mechanisms.
Two properties are worth knowing before you build anything on specgen:
-
Func.Nameis already the API name.//specgen:nameis resolved inside specgen, so consumers seeToBitsandReshapeToUint32s, never the underlying spec function names. Matching a concrete API declaration to a spec function is a plain(receiver type name, method name)lookup. No mapping table is needed anywhere. -
Func.Decl()already composes doc + signature, so anything that emits or compares declarations should build on it rather than reimplementing it.
Working in this tree
The _gen module requires at least the Go version listed in its go.mod. A
working Go dev tree or an installed Go system at that version is required.
Every generator supports -w (write into the tree), -diff (compare against
the tree and print unified diffs), and -txtar (dump generated files to stdout;
the default). Use -diff constantly. The generated files are checked in, so
byte-for-byte comparison against them is available as a regression test for any
change you make — including refactors that are supposed to change nothing.
Be sure to include changes to generated outputs in the same commit that changes the generator. This makes it easy to review the effects of the generator change.
Useful one-liners:
go run ./cmd/specls # the API spec currently expands to
go run ./cmd/specls -f Add # just one spec function
go run ./cmd/specls -f Add -trace # why a spec function expands the way it does
go run ./cmd/refgen -diff # is internal/simdref current?
go run . -tools tmplgen -diff # run one generator via the driver
Reading order for someone new
Working on spec? Check out:
simd/internal/spec/doc.go— the spec language:Vec,Width,//specgen:name,//specgen:require, doc templates.simd/internal/spec/math.go,loadstore.go,masks.go— worked examples.specgen/api.go—Func,Signature(),Decl(),SpecFunc().specgen/specexpr/solver.go— the shape constraint language, documented in the package comment.cmd/refgen/main.go— a simple consumer ofspecgen.Load, and a model for how to walk[]*Func.
Working on simdgen? Check out:
simdgen/ops/AddSub/*.yaml— unifier inputs for core math operations.simdgen/godefs.goandsimdgen/types/operation.go— theOperationmodel.