Skip to content

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

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.