Skip to content

floats

floats

CI coverage

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):

float64float32meaning
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.

Elementwise updates (BLAS level 1)

Since v0.4.0, the float64 in-place updates behind iterative solvers (CG, GMRES), Householder QR and LU elimination:

functioncomputes
Axpy(alpha, x, y)y[i] = math.FMA(alpha, x[i], y[i]) (y += αx)
Xpby(x, beta, y)y[i] = math.FMA(beta, y[i], x[i]) (y = x + βy)
AxpyDot(alpha, x, y, w)Axpy, then returns Σ y[i]*w[i] over the updated y, in one pass
Scale(alpha, x)x[i] *= alpha
// one conjugate-gradient step
floats.Axpy(alpha, p, x)               // x += αp
rr := floats.AxpyDot(-alpha, ap, r, r) // r -= αAp, rr = r·r
floats.Xpby(r, rr/rrOld, p)            // p = r + βp

They operate on the first len(x) elements and panic if y (or w) is shorter; y may be the same slice as x or w. The updated slices are bit-identical to math.FMA on every architecture: one rounding per element, via VFMADD (amd64 AVX2+FMA), VFMLA/FMADDD (arm64 NEON) or the portable math.FMA loop (ppc64le, s390x, riscv64, loong64, where no kernel has been measured on silicon yet). AxpyDot’s returned sum is a lane-blocked reduction, like Dot.

Speedup over the hand-written scalar loop (y[i] += alpha*x[i], …), single core, median of 3:

opn = 1e3 / 1e5 / 1e6, Apple M4 MaxAMD EPYC 7763
Axpy2.06× / 1.74× / 1.66×5.25× / 3.12× / 3.14×
Xpby2.01× / 1.58× / 1.65×5.25× / 3.10× / 3.14×
AxpyDot5.30× / 4.85× / 2.91×5.85× / 3.03× / 3.14×
Scale3.57× / 2.23× / 2.22×7.67× / 5.25× / 5.80×

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/cosinefloat32amd64arm64ppc64le / s390x / riscv64 / loong64
gonum/floatsdot, L2 only✗SSE✗✗
viterin/vek✓✓AVX2+FMA✗✗
kelindar/simd✗ (sum/min/max only)✓AVX2NEON✗
go-simd/floats✓✓AVX2+FMANEON✓ (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.

archISAFMA opnotes
amd64AVX2 + FMA3VFMADD231P{S,D}gated on cpu.X86.HasAVX2 && HasFMA; scalar fallback otherwise
arm64NEONVFMLA4 accumulators, ×4 unroll for ILP; baseline
riscv64RVVVFMUL+VFREDOSUMVSlength-agnostic VSETVLI stripmining; gated on cpu.RISCV64.HasV
s390xvector facility (big-endian)VFMADBfloat64 vectorised; float32 uses scalar (no .SB ops in assembler); z13 baseline
ppc64leVSX (float32 only)XVMADDASPXV* 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
loong64LSXvfmadd.d/vfmadd.sLSX 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):

opn=8n=64n=512n=4096
Dot float64 simd7.614.371.7539
Dot float64 naive2.418.51521075
Float32Dot simd9.511.739.4276

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/FuzzAxpy against the scalar reference on every architecture; the elementwise kernels also run against PROT_NONE guard pages on both sides of every slice. BSD-3-Clause.