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.yaml produces a single global sum. The ops/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.Def treats any absent field as Top, 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:

  1. Func.Name is already the API name. //specgen:name is resolved inside specgen, so consumers see ToBits and ReshapeToUint32s, 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.

  2. 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:

  1. simd/internal/spec/doc.go — the spec language: Vec, Width, //specgen:name, //specgen:require, doc templates.
  2. simd/internal/spec/math.go, loadstore.go, masks.go — worked examples.
  3. specgen/api.go — Func, Signature(), Decl(), SpecFunc().
  4. specgen/specexpr/solver.go — the shape constraint language, documented in the package comment.
  5. cmd/refgen/main.go — a simple consumer of specgen.Load, and a model for how to walk []*Func.

Working on simdgen? Check out:

  1. simdgen/ops/AddSub/*.yaml — unifier inputs for core math operations.
  2. simdgen/godefs.go and simdgen/types/operation.go — the Operation model.