floats¶
SIMD-accelerated reductions over []float32 and []float64 — the dot
product, sum, min, max, Euclidean (L2) distance and cosine similarity that sit at
the heart of vector search, embeddings and numerics. Pure Go
(CGO_ENABLED=0, stable Go, no GOEXPERIMENT). The hot reduction loops are real
FMA SIMD on all six of Go's 64-bit targets, assembly generated by
go-asmgen.
Repository →
API¶
For []float64 (top-level) and []float32 (the Float32* functions):
| float64 | float32 | meaning |
|---|---|---|
Dot(a, b) |
Float32Dot(a, b) |
a·b = Σ aᵢbᵢ |
Sum(a) |
Float32Sum(a) |
Σ aᵢ |
Min(a) / Max(a) |
Float32Min(a) / Float32Max(a) |
min / max (NaN-propagating) |
Distance(a, b) |
Float32Distance(a, b) |
‖a−b‖₂ = √Σ(aᵢ−bᵢ)² |
CosineSimilarity(a, b) |
Float32CosineSimilarity(a, b) |
a·b / (‖a‖₂‖b‖₂) |
import "github.com/go-simd/floats"
sim := floats.CosineSimilarity(query, doc) // []float64 relevance score
d := floats.Float32Distance(embA, embB) // []float32 L2 distance
dp := floats.Dot(a, b) // a·b
Dot, Distance, CosineSimilarity require len(a) == len(b) and panic
otherwise; Min/Max panic on an empty slice; Sum of empty is 0. The float32
dot/sum/squared-difference accumulate in float32; cosine and distance take the
final square root in float64 for accuracy.
Why this library¶
This is the only pure-Go float reduction library that ships the full vector-search API (dot / Euclidean / cosine) for both float32 and float64 on all six of Go's 64-bit SIMD architectures:
| dot/dist/cosine | float32 | amd64 | arm64 | ppc64le / s390x / riscv64 / loong64 | |
|---|---|---|---|---|---|
| gonum/floats | dot, L2 only | ✗ | SSE | ✗ | ✗ |
| viterin/vek | ✓ | ✓ | AVX2+FMA | ✗ | ✗ |
| kelindar/simd | ✗ (sum/min/max only) | ✓ | AVX2 | NEON | ✗ |
| go-simd/floats | ✓ | ✓ | AVX2+FMA | NEON | ✓ (all four) |
Algorithm & per-arch kernels¶
Every kernel is an FMA-based vectorised reduction: a multi-lane accumulator folded with a fused multiply-add at the vector stride, then a horizontal reduce and a scalar tail.
| arch | ISA | FMA op | notes |
|---|---|---|---|
| amd64 | AVX2 + FMA3 | VFMADD231P{S,D} |
gated on cpu.X86.HasAVX2 && HasFMA; scalar fallback otherwise |
| arm64 | NEON | VFMLA |
4 accumulators, ×4 unroll for ILP; baseline |
| riscv64 | RVV | VFMUL+VFREDOSUMVS |
length-agnostic VSETVLI stripmining; gated on cpu.RISCV64.HasV |
| s390x | vector facility (big-endian) | VFMADB |
float64 vectorised; float32 uses scalar (no .SB ops in assembler); z13 baseline |
| ppc64le | VSX (float32 only) | XVMADDASP |
XV ops WORD-encoded; POWER9-gated. float64 has no VSX kernel* — on real POWER9 the gc-autovectorized scalar loop beats VSX, so float64 routes to that loop |
| loong64 | LSX | vfmadd.d/vfmadd.s |
LSX FP ops WORD-encoded; LA464 baseline |
Floating-point determinism¶
A SIMD reduction does not add terms left-to-right: it keeps several independent
accumulator lanes (for ILP) and folds them at the end, so its rounding differs
from a naïve scalar loop. For dot / distance / cosine, bit-exactness against a
naïve loop is neither meaningful nor required — correctness to a few ULP is. The
reference the tests and fuzzers check against (dotLanes, sumSqDiffLanes, …)
reduces in the same lane-blocked order the kernels use, and each kernel must
match it to a tight, documented tolerance (1e-12 float64, 1e-5 float32) on
every architecture.
Performance — honest¶
Apple M-series (arm64, native), ns/op (lower is better); simd is this library,
naive is the textbook left-to-right loop (which the compiler already turns into
a pipelined scalar FMADD):
| op | n=8 | n=64 | n=512 | n=4096 |
|---|---|---|---|---|
Dot float64 simd |
7.6 | 14.3 | 71.7 | 539 |
Dot float64 naive |
2.4 | 18.5 | 152 | 1075 |
Float32Dot simd |
9.5 | 11.7 | 39.4 | 276 |
The SIMD dot product is ~2× the compiler's scalar loop on medium/large vectors (≈57 GB/s float64, ≈59 GB/s float32 at n=512), and loses only on tiny inputs (n ≤ 8) where unroll/setup overhead dominates.
amd64's AVX2+FMA kernel is correctness-validated on real x86 (an AVX2/FMA VM); native throughput is pending (the x86 runner here is TCG-emulated).
ppc64le — measured on real POWER9 (GCC Compile Farm cfarm433, Go 1.26.4, 2026-06-27): the float32 VSX kernel runs ~1.55–1.61× over the gc-autovectorized naive loop for n ≥ 64 (POWER9-gated), so it is kept. For float64, however, on real POWER9 the gc-autovectorized scalar loop beats the VSX kernel (VSX was ~0.82× at n ≥ 512), so float64 honestly routes to that loop and ships no VSX kernel — measured, not assumed. s390x, riscv64 and loong64 stay QEMU-validated for correctness, native perf pending real hardware.
Coverage¶
All six arches validated in CI — amd64/arm64 natively, the other four under QEMU
(tonistiigi/binfmt) — at 100% test coverage, with differential tests and
FuzzDot/FuzzDistance against the scalar reference on every architecture.
BSD-3-Clause.