Go's SIMD Experiment: Vector Math Without Assembly

Go is testing a portable SIMD API — one loop that becomes AVX on x86 and NEON on ARM. No assembly, no cgo, no per-arch forks.

Share

Every Go engineer who has profiled a hot numeric loop knows the moment: the CPU is bored, doing one add per cycle, while Rust and C scream past on vector lanes. Until now your escape hatch was assembly — written once per architecture, maintained forever.

The Go team just published an experiment that changes the deal: a platform-independent SIMD API. One source file. The compiler emits AVX on amd64 and NEON on arm64, and you never open a .s file again.

Why this matters

Go's compiler has never auto-vectorized loops. That pushed vector-hungry work onto three bad options: cgo into a C library (FFI overhead, broken cross-compilation), hand-written asm per GOARCH, or accepting 4-8x less throughput.

That compromise keeps getting more painful. Graviton and friends killed the "just target amd64" excuse. And Go now hosts exactly the workloads that want SIMD: embedding search, Parquet readers, checksums, image pipelines.

How it works

The simd package introduces fixed-size vector types like Vector[8, float64] — 512 bits treated as one value. You fill them with Load, do math with generic operations like Add, Mul, and MulAdd, and write back with Store.

The trick is that these types are opaque to you but meaningful to the compiler. Seeing simd.Add on amd64, it emits packed-add instructions; on arm64, the NEON equivalent. Narrower CPUs get the same result via more instructions. Your loop structure, bounds checks, and goroutines stay plain Go.

Where this helps

  • Vector search: cosine similarity over embeddings is a dot-product loop — the canonical case. Go RAG backends can drop the sidecar service or cgo dependency.
  • Data formats: CRC32C and xxHash-style checksums, Parquet/Arrow column scans, compression codecs — all lane-friendly.
  • Media pipelines: sRGB-to-linear conversion, resampling, audio mixing — the stuff currently stuck inside asm packages.
  • Analytics: columnar sum, min, max, and hash aggregation in Go-native query engines.

Watch out

  • It's an experiment behind GOEXPERIMENT=simd. The API can and will churn — don't expose simd types in a public library yet.
  • This is not auto-vectorization. You still chunk loops into lane-width blocks and handle the tail yourself. Small arrays or wrong lane counts can lose to plain scalar code.
  • Fixed widths don't map evenly across ISAs: one wide AVX op is several NEON ops. What wins on your laptop may crawl on Graviton — benchmark both.
  • If your hot loop already calls a tuned C library, this won't beat physics. It removes assembly from your codebase, not from the universe.

Try it yourself

A dot product over 8-wide lanes, tail included:

// Run with: GOEXPERIMENT=simd go run .
package main

import (
	"fmt"

	"simd"
)

type f64x8 = simd.Vector[8, float64]

func dot(a, b []float64) float64 {
	var acc f64x8
	for i := 0; i+8 <= len(a); i += 8 {
		va := simd.Load((*[8]float64)(a[i : i+8]))
		vb := simd.Load((*[8]float64)(b[i : i+8]))
		acc = simd.Add(acc, simd.Mul(va, vb))
	}
	sum := 0.0
	for _, lane := range acc.Array() {
		sum += lane
	}
	for i := len(a) &^ 7; i < len(a); i++ {
		sum += a[i] * b[i]
	}
	return sum
}

func main() {
	a := []float64{1, 2, 3, 4, 5, 6, 7, 8}
	b := []float64{8, 7, 6, 5, 4, 3, 2, 1}
	fmt.Println(dot(a, b)) // 120
}

TL;DR

  • Go is experimenting with a portable simd package: fixed-width vector types and math ops that compile to AVX or NEON from one codebase.
  • It matters because numeric Go — embeddings, codecs, analytics — can finally drop per-architecture assembly and cgo hot paths.
  • Build with GOEXPERIMENT=simd, benchmark the dot product above against your scalar loop, and watch the API for churn before shipping it.