simd

package module
v1.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 17 Imported by: 0

README

simd.go

Latest Release Go Reference CI

Fast slice math and text scanning for Go, using the vector unit your CPU already has — without cgo.

The project is simd.go. The import path is github.com/sebishogun/simd and the package is simd.

Install

go get github.com/sebishogun/simd

Go 1.25 or later. No cgo, no C toolchain, no build tags, no GOEXPERIMENT. The kernels are compiled ahead of time and committed as assembly, so this is an ordinary Go dependency with one transitive import (golang.org/x/sys, for CPU feature detection).

Quick start

import "github.com/sebishogun/simd"

simd.Add(a, b)                     // a[i] += b[i]      — in place, no allocation
simd.AddScaled(a, b, 0.5)          // a[i] += b[i]*0.5  — one pass over memory
total := simd.Sum(a)               // fixed accumulation order, same bits everywhere
i     := simd.Index(line, ",")     // takes string or []byte, no copy
simd.GemvInto(y, matrix, x, m, k)  // matrix times vector

Generic functions over ordinary Go slices. There is no vector type, no lane count, no target to select, and nothing to initialize. The best available instruction set is detected once at startup.

Two conventions cover the whole API:

simd.Add(a, b)          // plain name: in place, a[i] += b[i]
simd.AddInto(dst, a, b) // Into suffix: dst[i] = a[i] + b[i], inputs untouched

Nothing allocates. Into functions take the destination from the caller precisely so they do not have to allocate one.

Documentation

Function index

Organised by task rather than by operation name.

I want to… Call
add/scale/clamp a slice in place Add Scale AddScalar Clamp
…without destroying the input the same name with Into
do y += a*x in one pass (axpy) AddScaled
sum or multiply many slices at once AddAll MulAll — one pass, not one per slice
sort a slice Sort SortInto (allocation-free)
sort one slice by another's values Argsort + GatherInto
split a slice about a threshold PartitionInto — stable on both sides
total / average / spread of a slice Sum Mean StdDev Variance
length of a vector, distance between two Norm Distance CosineSimilarity
make a vector unit length Normalize
smallest and largest in one pass MinMax, or ArgMin/ArgMax for positions
keep only the elements that pass a test a comparison → []bool, then CompressInto
apply an arbitrary Go predicate FilterInto (convenient, not fast — see its doc)
running totals / differences CumSum DiffInto
exp/log/trig over a slice Exp Log Sin … and the Fast* twins
pick between two slices per element SelectInto
apply a matrix to a vector GemvInto, or GemvParallelInto for a large one
multiply two matrices MatMulInto, or MatMulParallelInto when the multiply is the whole job
add an outer product to a matrix RankOneInto — BLAS ger, the inner loop of an LU
apply a Givens rotation Rotate — what QR and the SVD are made of
exchange two slices Swap — pivoting a decomposition
find a byte or substring IndexByte Index LastIndex
find every occurrence at once IndexAll, or IndexAllAny for a set of delimiters in one pass
the same, as a bitmask MaskBits MaskBitsAny MaskBitsLess — a bit per byte, 22–37× the offset list when matches are dense
find any of a set of bytes IndexAny IndexNotAny CountAny
the next byte that is not plain text IndexAnyOrLess — a set and a threshold in one pass, the inner loop of an escape routine
trim, fold case, validate UTF-8 TrimAny TrimSpaceASCII EqualFoldASCII ValidUTF8
hex or base64 HexEncode Base64Encode Base64Decode
convert to/from float16 or bfloat16 Float16ToFloat32Into and friends
median / percentile without sorting Median Quantile, MedianInto for zero-alloc
the k largest or smallest TopK BottomK — selects, does not sort
histogram / count occurrences Histogram Bincount
find the NaNs, sum around them IsNaNInto CountNaN NanSum NanMean
shifts, rotates, popcount per element Shl Rotl OnesCountInto LeadingZerosInto ByteSwapInto
a Fourier transform FFTInto with a reusable plan; RFFT for real input
envelope / analytic signal HilbertInto, then AbsComplexInto
window a signal Hann Hamming Blackman, then ApplyWindowInto
convolve or correlate ConvolveFullInto — picks direct or FFT by a measured crossover
interpolate a table InterpInto — numpy's interp, clamping
transpose a matrix TransposeInto — blocked, 3.6× the naive loop
parse a CSV of integers IndexAll + ParseInts — 5× strconv
quantize a tensor to int8 QuantizeInt8, or QuantizePerChannelInt8 for weights
multiply int8 tensors QMatMulInt8Into → int32, then RequantizeInt8Into
normalize a transformer layer LayerNorm, or LayerNormInto with gamma and beta
look up many keys in a sorted table LowerBoundInto — one binary search per query, batched
how much two byte slices share CommonPrefixLen — the LCP step of a suffix array
rolling minimum or maximum RollingMinInto RollingMaxInto — see the window note in its doc
intersect or subtract sorted sets IntersectInto DifferenceInto — posting lists
rank/select over a bit vector RankTableInto, then Rank and Select
size a varint stream before writing it VarintSize VarintLenInto AppendVarints
multiply a sparse matrix by a vector SpMVInto, or SparseDot for one CSR row
convert to/from fp8 Float32ToFloat8E4M3Into and the e5m2 pair
make negative deltas small ZigzagEncodeInt32Into, before varint
pack a column densely DiffIntoZigzagEncodeInt32IntoBitPackInto
run-length encode a column RunLengthEncodeInt32, or RunStartsInto for the mask alone
compare two bit vectors HammingDistance — fused, not Xor then PopCount
convert planar RGB GrayscaleInto, RGBToUVInto
sum data arriving in chunks Accumulator[T] — bit-identical to Sum of the whole
fill a slice with random values RandomInto — reproducible, splittable, same everywhere

What is included

Any operation a vector unit can do faster than a scalar loop. An operation is absent only because it was measured and lost, or because nobody has built it yet; both are recorded rather than implied.

  • Array math — elementwise and saturating arithmetic, scalar operations, rounding, comparisons to masks, prefix scans.
  • Reductions — sum, dot, min/max, argmin/argmax, norms, mean, variance.
  • Transcendentals — exp, log, sin, tan, tanh, sigmoid and the rest, each with a stated ULP bound and a Fast* twin.
  • Text and bytes — index, count, trim, UTF-8 validation, case folding, hex, base64, accepting string or []byte without copying.
  • Linear algebra — dot, Gemv, a register-blocked MatMul with an opt-in parallel variant, CSR sparse matrix-vector, and the pieces a decomposition is made of: rank-1 update, Givens rotation, swap.
  • Search and sets — batched binary search, sorted-set intersection and difference, rank/select over a bit vector, longest common prefix.
  • Encodings — int8 and fp8 quantization, zigzag, bit packing, run-length, varint widths.

Element types: float32 float64 int8 int16 int32 int64 uint8 uint16 uint32 uint64 complex64 complex128, plus bytes, text and the narrow float formats.

Not a BLAS, a tensor library, or an autodiff framework. It has no opinion about your data layout beyond "it is a slice" and does not own your control flow.

Where the win is

The crossover is roughly 16 to 64 elements depending on the operation. Below it the library runs a plain Go loop, because crossing into assembly costs about 1.4 ns and cannot be inlined. Reach for this when slices are thousands of elements, not tens. Calling a vector operation once per element in a loop is slower than not using the library at all — see the tutorial.

Most operations take and return whole slices. Exposing a vector value that callers combine themselves costs a non-inlinable call per operation and loses to a plain loop; that is measured, not stylistic. Whole-slice is the default shape rather than a hard boundary — HexDecode and CompressInto already have data-dependent output lengths. For operations too small for the call boundary, there is a goexperiment.simd vector type on amd64 and arm64.

Accuracy

Every operation is bit-identical across instruction sets, including NaN payloads, ±Inf, ±0 and denormals. Reductions use a fixed sixteen-accumulator tree that a 128-bit and a 512-bit machine both reproduce exactly, so a result cannot change because the computation moved to a different server. Dot never contracts into FMA on any architecture, including those where Go's compiler would.

This costs throughput deliberately. The alternative is what vek does: its vectorized body and scalar remainder disagree on NaN, so the answer depends on input length.

Two exceptions, both opt-in by name:

  • Transcendentals guarantee a ULP bound rather than bit identity, because the polynomial correct to 1 ULP in float32 is not the one correct to 1 ULP in float64. The bound is measured against the standard library, not asserted from theory.
  • Fast* promises 3.5 ULP and gives up agreement between architectures, because it compiles with fused multiply-add. It keeps IEEE 754 semantics: NaN in gives NaN out, infinities go where the standard says, signed zeros survive. -ffast-math is not used.

Performance

Measured on a Ryzen AI MAX+ 395 (Zen 5, AVX-512), -count 6 or more, compared with benchstat. Regenerate with make bench-check. Every number here is amd64 — see platform support for why there are no others.

Against the portable Go build. Integer and saturating arithmetic, geomean over the whole set: −86% time, +593% throughput.

int8 SatAdd portable accelerated
n=256 146 ns 6.6 ns −95%
n=4096 2.42 µs 22.0 ns −99.1%

Against bytes and strings. The harder comparison, since bytealg is already hand-written assembly on four of the six architectures. Geomean +186%.

vs stdlib
LastIndex n=4096 +8309%
IndexAny n=1 MiB +1084%
Index n=1 MiB +623%
IndexAll n=1 MiB +135%
ValidUTF8 n=1 MiB, non-ASCII +332%
ValidUTF8 n=1 MiB, ASCII +58%
TrimSpaceASCII +29%

Against encoding/base64: −42% to −63%, +74% throughput.

MatMulInto, register-blocked, against the previous naive kernel. Single-core AVX-512 float32 peak on this machine is about 290 GFLOP/s, so the right-hand column is the one that matters:

f32, square naive blocked GFLOP/s
n=64 9.51 µs 2.13 µs −78% 246
n=128 51.7 µs 16.9 µs −67% 249
n=256 331 µs 127 µs −62% 264
n=512 3.25 ms 1.31 ms −60% 204

GemvInto reaches 172 GB/s while the matrix is cache-resident and 49 GB/s at 4096×4096, where it is bound by memory rather than arithmetic.

CompressInto against the scalar filter loop, geomean −51%. Match density is the axis that matters, and a single-density benchmark misleads in either direction: the scalar loop costs a branch per element, so it is fastest exactly when that branch is predictable.

1 M int32 scalar loop CompressInto
1% match 12.3 GiB/s 19.3 GiB/s −36%
25% match 2.17 GiB/s 19.4 GiB/s −89%
50% match 1.29 GiB/s 19.3 GiB/s −93%
90% match 4.12 GiB/s 19.1 GiB/s −78%

The vector column barely moves across densities. The scalar column collapses with branch prediction.

Fast against accurate: FastSin −45%, FastExp −43%, FastSigmoid −36%, FastLog −25%.

Where the standard library is already assembly doing the same work — bytes.Equal is memequal, bytealg.Count popcounts a compare mask — there is no margin, and this library calls it instead.

Measuring on your own machine
go run ./cmd/site      # http://localhost:8080

Runs the docs/tutorial.md comparisons live, shows both implementations side by side, and reports the minimum of several samples with the detected tier and load average printed alongside. It warns when the machine is busy. Datastar is vendored in cmd/site/assets/; nothing contacts a CDN.

Platform support

architecture tiers correctness wall-clock
amd64 sse2, avx2, avx512 real hardware real hardware
arm64 neon real hardware unmeasured
arm64 sve2 emulation unmeasured
riscv64 rvv emulation unmeasured
ppc64le vsx emulation unmeasured
s390x vx emulation unmeasured
loong64 lasx emulation unmeasured

Every kernel is differential-tested against the portable implementation on every architecture on every change, which is what catches a wrong answer. Emulation proves nothing about timing, because qemu does not model a pipeline. Where the table says unmeasured, no performance claim in this repository applies.

Throughput off amd64 is modelled rather than measured: make perf-model runs llvm-mca over each kernel's inner loop for arm64, ppc64le and s390x. Nothing models below 1.2×, and the model agrees with measured amd64 to within 5–12% on the avx512-versus-avx2 comparison. No model gives wall-clock under a real memory system, which is what dominates a whole-slice kernel at large n.

Have one of these machines? A single real run is worth more than any amount of emulation, and it takes two commands. Failures are as useful as passes. See Reporting a hardware run. No knowledge of the internals is needed, and the table above moves as reports arrive.

The library has no OS-specific source. It builds and vets clean for darwin/amd64, darwin/arm64, windows/amd64, windows/arm64 and freebsd/amd64; the entire OS-dependent surface is x/sys/cpu feature detection.

Kernel coverage

468 exported functions and 6,757 generated kernels across nine targets. The function count is for an ordinary build; the goexperiment.simd vector type adds four more. Kernel counts come from make check-emission. The skip column is kernels the generator declined with a stated reason, not kernels nobody wrote. Both columns sum over an architecture's tiers, so a kernel declined on sse2 and emitted on avx2 counts once in each.

kernels skipped dominant reason for skips
amd64 (sse2/avx2/avx512) 2530 90 LLVM declined to vectorize
arm64 (neon/sve2) 1631 117 LLVM declined to vectorize
riscv64 (rvv) 862 14 LLVM declined to vectorize
loong64 (lasx) 717 155 $fp, then .L0 references
ppc64le (vsx) 598 274 r30, then LLVM refusals
s390x (vx) 419 453 r13 — an ABI wall, see below

Most remaining skips are in the Fast* tier, which is the newest and least portable. Where a target declines a Fast* kernel the accurate kernel stands in, which still satisfies the promise because 3.5 ULP is an upper bound.

The ABI walls, in order of how much they cost:

  • s390x loses 405 of its 446 skips to r13. clang allocates it; Go keeps the current goroutine there. SystemZ has no -ffixed equivalent — the global register variable is accepted and silently ignored.
  • ppc64le loses most of its skips to r30, which Go uses for g, plus a smaller group where clang writes a nonzero value to r0. The ppc64le ABI defines r0 as constant zero, so a signal arriving before the epilogue would run the runtime with a poisoned zero register. The TOC pointer (r2) used to cost 184 kernels and now costs 8: the constant pool became a standalone symbol addressed in two instructions, and clang's global-entry prologue is replaced in place. That took ppc64le from 281 kernels to 592.
  • loong64 loses 92 to $fp and 28 to .L0 references, where clang points at a label that is not a constant pool and so cannot be lifted the way a pool can. A further group emits branches with a displacement of zero for the linker to fill in, which the generator would have to compute and patch — bounded work nobody has done. Copying them verbatim, which is correct on AArch64 and RISC-V, produces branches to themselves.

None of this is a correctness hole. A kernel that cannot be generated is not registered and the portable implementation runs instead. The differential suite compares whatever a tier actually ended up with against that reference, so a skipped kernel is slower and never wrong.

Testing

make menu lists every verification target and marks the ones the current machine can run:

$ make menu
gosimd  make targets on darwin/arm64
        arm64 tier=neon available=[scalar neon]
        go clang llvm-mca llvm-objdump docker benchstat qemu-riscv64 …

● test-tiers      Run the suite once per instruction-set tier this CPU has
● perf-model      Model kernel throughput on architectures this machine …
○ test-riscv64    Full suite on riscv64 RVV under qemu
○ test-loong64    Full suite on loong64 LASX under qemu

Roughly a third of the targets cannot run on any given machine — the qemu lanes need a Linux host, the cross lane needs docker with binfmt, codegen needs clang and llvm-objdump. The preview says which, why, and what to install. make targets prints the same list plainly. Neither requires fzf; it is used when present.

What runs:

  • Differential testing of every generated kernel against the portable reference, at every length from 0 to 70 and at block boundaries beyond, with adversarial inputs: NaN, ±Inf, ±0, denormals, the extremes of every integer type.
  • Tier against tier, so the promise that results do not vary with vector width is checked directly rather than inferred.
  • Fuzzing across the kernel set, millions of executions per run.
  • Gate-versus-emission: every generated .s is disassembled and checked against the CPU feature its file is gated on, so an EVEX instruction cannot reach an AVX2 path. This prevents the SIGILL class of bug mechanically.
  • ABI checks: no kernel may use a register the Go runtime owns, write outside its frame, or leave a reserved register modified.
  • Execution on every architecture under emulation, with simdinfo -require-accelerated asserting that an accelerated tier was actually selected before a green run is believed.

That last check exists because its absence cost two backends. The riscv64 and loong64 lanes were green for months while executing nothing: the emulator in the image predated the vector extension, so every tier was skipped as unexecutable and the suite passed having tested none of it. The first run that actually executed them found a segfault in one and wrong answers from every constant-reading kernel in the other.

Repository layout

.                    the simd package — the public API, one file per topic
csrc/                the kernels, in C. The source of truth for every fast path
internal/
  amd64/ arm64/ …    generated Plan 9 assembly, one directory per architecture
  ref/               the portable Go implementation everything is tested against
  kernel/            dispatch table and the numerical contract
  cpu/               feature detection and tier selection
  conformance/       the differential suite: every tier against ref, and each other
  asmcheck/          static assertions on the committed assembly
  benchmarks/        every benchmark
  tests/             the public-API test suite, by topic:
                       arrays reduce text search encode dsp matrix docs
cmd/simdinfo/        prints the tier actually selected on this machine
cmd/site/            local benchmark site
tools/               the code generator — a separate module, never your dependency
docs/                tutorial, guide, kernels.md, wrong.md, examples
testdata/bench/      recorded benchmark baseline, per GOARCH
testdata/hardware/   one report per machine that has run on real silicon

The C is the source; the assembly is the output. A kernel is written once in csrc/, compiled per instruction set by tools/, and committed under internal/ so that using this library needs no C toolchain. Every generated .s names the C file it came from and the target it was built for, and none of them should be edited by hand — make codegen regenerates them.

Tests live next to what they can reach. Anything that only calls the public API is in internal/tests/<topic>/; what stays in the root directory is what Go requires to be there — tests of unexported behaviour, the export_test.go hooks and the files using them, and the runnable examples, which have to sit beside the package to appear on pkg.go.dev.

Every directory above has a README explaining what is in it.

Development

Consumers need nothing beyond go get. Regenerating the assembly needs clang and llvm-objdump. The generator lives in a nested module so it never becomes a dependency of anyone using the library.

make verify        # fmt, vet, tests, purego build, every tier this CPU can run
make test-cross    # arm64, s390x, ppc64le under docker + qemu
make test-riscv64  # cross-compile and run under a recent qemu-user
make test-loong64  # likewise; there is no golang image for loong64
make bench-check   # benchmarks against the stored baseline for this GOARCH
make codegen       # regenerate every backend (needs clang)

Built on this

Four libraries use these kernels. Each is a case where the vector unit changes what the code can do, rather than a wrapper around the API.

simdblas A BLAS backend for gonum One blas64.Use call and mat, stat and optimize run on it. Covariance plus Cholesky 4.4×, QR 2.0×, mat.Mul 4.3×.
simdjson Structural-index JSON parsing 6.6× encoding/json for field extraction, and 1.4–1.7× minio/simdjson-go — which is amd64-only. Its structural pass is built on MaskBits.
simdcsv CSV reading 1.7–2.1× encoding/csv on unquoted data, with fields as subslices rather than copies.
simdvec Embedding search 18–38× a hand-written loop; the whole index scan is one matrix-vector product.

simdjson is the one worth reading if you are deciding whether any of this is real. It was 1.3–1.8× slower than minio until benchmarking against it named the missing primitive — IndexAll took a single byte, so six JSON delimiters meant six reads of the document. IndexAllAny closed it in one release, and profiling the result named the next one: on input where matches are common, a list of offsets is larger than the input and the store is most of the work. The MaskBits family in v1.5.0 came from that, and took simdjson's structural pass from 1.76 ms to 370 µs.

Why this exists

The existing Go options leave most machines unserved.

instruction sets operations same answer on every one
gonum internal/asm SSE2 only — zero V* instructions in the whole repo linear algebra
viterin/vek AVX2 only, disabled entirely on macOS, arm64 is pure Go and never will not be broad no
kelindar/simd AVX2, NEON 7 no
this sse2, avx2, avx512, neon, sve2, rvv, vx, lasx, vsx 457 yes

kelindar/simd deserves singling out, because it is the closest relative: it reaches its two instruction sets the same way this does, by auto-vectorizing C with clang and translating the result into Plan 9 assembly, and it dispatches on a runtime CPU check rather than a build tag. If AVX2 and NEON are all you need and Add, Sub, Mul, Div, Sum, Min and Max are all you want, it is a smaller dependency with a lower Go floor, and there is nothing wrong with it.

The difference beyond scope is what the answer is allowed to do. Summing a float32 slice of one large value and a thousand small ones:

                        accelerated          portable
kelindar/simd           0x4cbebc9c    ≠      0x4cbebc20
this library            0x4cbebc98    =      0x4cbebc98

Reproduce it with docs/comparison, a separate module so that nothing you import pulls in a second SIMD library.

That is not a defect in kelindar — it never promises otherwise, and an auto-vectorized reduction naturally accumulates in whatever order the vector width implies. It does mean a result computed there can change when the binary moves to a machine with different SIMD support. This library fixes the accumulation order instead, which is what the accuracy contract above costs throughput to buy.

There is also a structural reason nobody covers the rest: Go's assembler cannot spell SVE2 or RVV. It has no Z or P registers at all, and upstream has deferred scalable vectors with no design. Compiling C per target and lifting the encoded bytes into Plan 9 assembly is the only route that reaches them, and it is why this is the only Go library with arm64 SVE2 and riscv64 RVV numeric kernels.

Where the obvious answer was wrong

docs/wrong.md records 74 things a competent person would have assumed that turned out false, and what each cost. Among them:

  • A register can be reserved by value rather than by name, which makes it invisible to every compiler flag. The symptom is Go's allocator dying several calls later.
  • A compiler builtin that silently compiles to nothing at -O1 and above, and is correct at -O0.
  • Green test lanes that had been executing no accelerated code for months.
  • Four loops that were slower after being vectorized, one by 1700×.
  • --mattr=+sve2 removing NEON rather than adding SVE2.
  • Go's own SIMD intrinsics being 4.4× slower than the generated assembly.
  • A closure comparator costing a sort 2.5×, and the wrong fix making it worse.
  • A scripted edit that silently did not apply, and would have crashed most CPUs.
  • A failing test naming a kernel that was not the one with the bug.
  • A test lane that was hung, not slow — thirty-two minutes at 0.1% CPU.
  • ENOSPC with 40 GB free.

docs/research/ carries the longer reasoning behind design decisions; 05-decisions.md is the decision record.

Status

v1.5.0. The API is stable: every exported function keeps its name, signature and meaning for the life of v1, and so does the numerical contract above. CHANGELOG.md states exactly what compatibility covers and what it excludes. ROADMAP.md lists what is still open.

License

MIT — see LICENSE. Use it in anything, including commercially; keep the copyright notice.

The only dependency is golang.org/x/sys (BSD-3-Clause), for CPU feature detection. The benchmark site vendors the Datastar browser bundle, which is MIT and carries its own notice in cmd/site/assets/; it is not part of the library and nothing you import pulls it in.

Documentation

Overview

Package simd provides SIMD-accelerated operations over slices, on every architecture that has a vector unit, without cgo.

You call ordinary Go functions on ordinary Go slices. There is no vector type, no lane count, no target selection, and nothing to initialize.

In place by default

The plain name operates in place on its first argument, which allocates nothing and reuses memory you already have:

simd.Add(a, b)        // a[i] += b[i]
simd.Scale(a, 2.5)    // a[i] *= 2.5
simd.Abs(a)           // a[i] = |a[i]|

When the result belongs somewhere else, the same name with an Into suffix takes a destination first:

simd.AddInto(dst, a, b)      // dst[i] = a[i] + b[i]
simd.ScaleInto(dst, a, 2.5)

Reductions return a single value and write nothing:

total := simd.Sum(a)
d := simd.Dot(a, b)
lo, hi := simd.MinMax(a)

This package never allocates. Every function writes only into memory the caller supplied, and no variant returns a freshly made slice. If you want one, make it yourself and use the Into form.

The functions are generic over float32, float64, int32 and int64, so the element type is inferred and there are no per-type name suffixes.

The right instructions are chosen for you

The instruction set is selected once, at process start, from the CPU the program is actually running on. A binary built on a laptop with AVX-512 runs correctly on a server without it, using AVX2 or SSE2 instead; on an architecture with no backend it uses portable Go. Nothing to configure, nothing to build twice.

Results do not depend on the hardware

Every operation returns bit-identical results on every instruction set, including for NaN, ±Inf, ±0 and denormals. Reductions such as Sum and Dot use a fixed accumulation order that a 128-bit and a 512-bit machine both reproduce exactly, so a computation cannot change answer because it moved to a different server. Operations that trade this away for speed are named Fast* and say so.

This costs some throughput and is deliberate: the alternative is a library where results shift with slice length or CPU model, which is the most common bug in this class of package.

Sizing

Lengths need not match. Every operation processes the minimum length of its slice arguments, so slicing is how you bound work:

simd.AddInto(dst[:n], a, b)

Small inputs use an inlined scalar path rather than a call into assembly, because below roughly sixteen elements the call costs more than the arithmetic saves. You do not need to check for this.

Environment

GOSIMD names an instruction-set tier to use instead of the detected one, for benchmarking and debugging: GOSIMD=sse2, GOSIMD=avx2, GOSIMD=scalar. It can only select down; naming a tier the CPU lacks falls back to portable Go rather than crashing.

SIMD_DISABLE masks tiers out of consideration, as a comma-separated list: SIMD_DISABLE=avx512. Useful on CPUs where wide vectors cause frequency throttling.

Tier and Describe report what was selected.

Example

The plain name works in place and allocates nothing.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float32{1, 2, 3, 4}
	b := []float32{10, 20, 30, 40}

	simd.Add(a, b) // a += b
	fmt.Println("a += b:", a)

	simd.Scale(a, 0.5) // a *= 0.5
	fmt.Println("a *= .5:", a)

	fmt.Println("sum:", simd.Sum(a))
	fmt.Println("max:", simd.Max(a))

}
Output:
a += b: [11 22 33 44]
a *= .5: [5.5 11 16.5 22]
sum: 55
max: 22
Example (Bytes)
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	b := []byte("hello world")
	fmt.Println(simd.IndexByte(b, 'w'))
	fmt.Println(simd.CountByte(b, 'l'))
	fmt.Println(simd.Equal(b, []byte("hello world")))

	data := []byte{0xab, 0xcd, 0xef}
	mask := []byte{0x0f, 0x0f, 0x0f}
	simd.And(data, mask) // in place
	fmt.Printf("%x\n", data)

}
Output:
6
3
true
0b0d0f
Example (FeatureScaling)

Scaling features into [0,1] — the pass in front of most models, and three calls with no temporary.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	raw := []float32{-5, 0, 12, 7, 30}

	lo, hi := simd.MinMax(raw)
	simd.AddScalar(raw, -lo)   // shift the minimum to zero
	simd.Scale(raw, 1/(hi-lo)) // and the maximum to one
	simd.Clamp(raw, 0, 1)      // rounding at the ends cannot escape

	for _, v := range raw {
		fmt.Printf("%.2f ", v)
	}
	fmt.Println()

}
Output:
0.00 0.14 0.49 0.34 1.00
Example (Fused)

AddScaled is AXPY: one pass over memory instead of two.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float32{1, 2, 3, 4}
	b := []float32{10, 10, 10, 10}

	simd.AddScaled(a, b, 0.5) // a += b * 0.5
	fmt.Println(a)

}
Output:
[6 7 8 9]
Example (Into)

Use the Into form when the result belongs somewhere else.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3, 4}
	b := []float64{10, 20, 30, 40}
	dst := make([]float64, 4)

	simd.AddInto(dst, a, b)
	fmt.Println(dst)
	fmt.Println(a, "unchanged")

}
Output:
[11 22 33 44]
[1 2 3 4] unchanged
Example (Scenarios)

Whole tasks are one call and reuse the same accelerated primitives.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{2, 4, 4, 4, 5, 5, 7, 9}

	fmt.Printf("mean   %.2f\n", simd.Mean(a))
	fmt.Printf("stddev %.2f\n", simd.StdDev(a))

	x := []float64{1, 0}
	y := []float64{0, 1}
	fmt.Printf("cosine %.2f\n", simd.CosineSimilarity(x, y))
	fmt.Printf("dist   %.4f\n", simd.Distance(x, y))

	v := []float64{3, 4}
	simd.Normalize(v)
	fmt.Println("unit  ", v)

}
Output:
mean   5.00
stddev 2.00
cosine 0.00
dist   1.4142
unit   [0.6 0.8]
Example (Sizing)

Lengths need not match; slicing bounds the work.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3, 4, 5, 6, 7, 8}
	b := []float64{1, 1, 1, 1, 1, 1, 1, 1}

	simd.Add(a[:3], b) // only the first three
	fmt.Println(a)

}
Output:
[2 3 4 4 5 6 7 8]
Example (Types)

The element type is inferred; there are no per-type function names.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	i64 := []int64{1 << 40, 2 << 40}
	simd.Add(i64, i64)
	fmt.Println(i64)

	f64 := []float64{0.5, 0.25, 0.125}
	fmt.Println(simd.Sum(f64))

	i32 := []int32{-3, 7, -1}
	simd.Abs(i32)
	fmt.Println(i32)

}
Output:
[2199023255552 4398046511104]
0.875
[3 7 1]

Index

Examples

Constants

View Source
const HasVectorType = false

HasVectorType reports whether this build has the vector type — that is, whether it is amd64 and GOEXPERIMENT=simd is set. It is false here.

It is a constant, so a caller can branch on it and have the dead side compiled away:

if simd.HasVectorType {
	// ... vector path
} else {
	// ... slice API, which is accelerated either way
}

Variables

This section is empty.

Functions

func Abs

func Abs[T Number](a []T)

Abs replaces every element with its absolute value.

For floats this clears the sign bit, so -0 becomes +0 and NaN keeps its payload. For integers it wraps: the absolute value of the most negative value is itself, matching the hardware instruction.

func AbsComplexInto

func AbsComplexInto[R Float, C Complex](dst []R, a []C)

AbsComplexInto writes the magnitude of each element of a into dst.

The magnitude is formed through the larger component, so an intermediate cannot overflow for a result that is representable — the same reasoning as Hypot, and it matters more here because both components are caller data.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 2)
	simd.AbsComplexInto(dst, []complex128{complex(3, 4), complex(0, 1)})
	fmt.Println(dst)
}
Output:
[5 1]

func AbsInto

func AbsInto[T Number](dst, a []T)

AbsInto sets dst[i] to the absolute value of a[i]. See Abs for semantics.

func Acos

func Acos[T Float](a []T)

Acos replaces every element with its arccosine, in radians. Inputs outside [-1, 1] yield NaN.

func AcosInto

func AcosInto[T Float](dst, a []T)

AcosInto sets dst[i] to the arccosine of a[i]. dst may alias a.

func Acosh

func Acosh[T Float](a []T)

Acosh replaces every element with its inverse hyperbolic cosine. It is NaN below 1, where the function is undefined.

func AcoshInto

func AcoshInto[T Float](dst, a []T)

AcoshInto sets dst[i] to the inverse hyperbolic cosine of a[i], which is NaN below 1. dst may alias a.

func Add

func Add[T Number](a, b []T)

Add adds b into a, elementwise: a[i] += b[i].

It processes min(len(a), len(b)) elements and allocates nothing. Use AddInto to write the result elsewhere.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3}
	simd.Add(a, []float64{10, 20, 30})
	fmt.Println(a)
}
Output:
[11 22 33]

func AddAll

func AddAll[T Number](dst []T, srcs ...[]T)

AddAll sets dst[i] to the sum of the corresponding element of every source, making a single pass over memory rather than one per source.

The sum is accumulated left to right, so the result is bit-identical to writing the binary calls out by hand. Floating-point addition is not associative, so that is a real guarantee and not a formality: reordering the sources changes the answer, and this function will not reorder them.

The work is bounded by the shortest slice, dst included. With no sources dst is zeroed; with one it is copied.

Sources beyond the fourth are folded in groups, so a sixteen-way sum is four passes rather than fifteen.

Why there is no general ZipInto(dst, f, srcs...)

Because it would be slower than the loop you would write without it. A closure cannot be vectorized, so the combinator's only advantage is making one pass instead of several — and that is not nearly enough. Measured on dst = a*b + c, nanoseconds:

n           your own loop   this catalogue   ZipInto with a closure
1024                728.7             52.6                     1163
262144             195722            60707                   310461
4194304           4377370          3597582                  5446108

The ZipInto column is the *generous* version, specialized to a fixed arity so there is no per-element argument slice; the honest variadic form is another 1.6x to 2.6x worse again. It loses to a plain Go loop at every size.

So the guidance is: use these, and where your expression is not here, write the loop. AddScaled covers a*s + b, MulAll and AddAll cover the n-ary products and sums, and a loop you write yourself will beat any closure this package could call on your behalf. See entry 47 of docs/wrong.md.

Example

AddAll sums any number of slices in one pass over memory, rather than one pass per slice.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 3)
	simd.AddAll(dst,
		[]float64{1, 2, 3},
		[]float64{10, 20, 30},
		[]float64{100, 200, 300})
	fmt.Println(dst)
}
Output:
[111 222 333]

func AddComplex

func AddComplex[C Complex](a, b []C)

AddComplex adds b into a elementwise: a[i] += b[i].

It processes min(len(a), len(b)) elements and allocates nothing. Use AddComplexInto to write the result elsewhere.

func AddComplexInto

func AddComplexInto[C Complex](dst, a, b []C)

AddComplexInto sets dst[i] = a[i] + b[i]. dst may alias a or b.

func AddInto

func AddInto[T Number](dst, a, b []T)

AddInto sets dst[i] = a[i] + b[i].

It processes min(len(dst), len(a), len(b)) elements. dst may alias a or b.

func AddScalar

func AddScalar[T Number](a []T, s T)

AddScalar adds s to every element: a[i] += s.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3}
	simd.AddScalar(a, 100)
	fmt.Println(a)
}
Output:
[101 102 103]

func AddScalarInto

func AddScalarInto[T Number](dst, a []T, s T)

AddScalarInto sets dst[i] = a[i] + s. dst may alias a.

func AddScaled

func AddScaled[T Number](a, b []T, s T)

AddScaled adds a scaled b into a: a[i] += b[i] * s.

This is the AXPY of BLAS, and it is the reason a fused catalogue exists: written as separate Mul and Add calls the same work costs two passes over memory, which is what makes naive slice libraries memory-bound.

Example

AddScaled is axpy: y += a*x, in one pass rather than a multiply pass and an add pass.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	y := []float64{1, 2, 3}
	simd.AddScaled(y, []float64{10, 10, 10}, 0.5)
	fmt.Println(y)
}
Output:
[6 7 8]

func AddScaledInto

func AddScaledInto[T Number](dst, a, b []T, s T)

AddScaledInto sets dst[i] = a[i] + b[i]*s in a single pass over memory.

func All

func All(m []bool) bool

All reports whether every element is true. An empty mask is vacuously true.

func And

func And(a, b []byte)

And clears in a every bit not set in b: a[i] &= b[i].

It processes min(len(a), len(b)) bytes and allocates nothing. Use AndInto to write the result elsewhere.

func AndInto

func AndInto(dst, a, b []byte)

AndInto sets dst[i] = a[i] & b[i].

It processes min(len(dst), len(a), len(b)) bytes. dst may alias a or b.

func AndMask

func AndMask(a, b []bool)

AndMask sets a[i] = a[i] && b[i], in place.

func AndMaskInto

func AndMaskInto(dst, a, b []bool)

AndMaskInto sets dst[i] = a[i] && b[i]. dst may alias a or b.

func AndNot

func AndNot(a, b []byte)

AndNot clears in a every bit set in b: a[i] &^= b[i].

It processes min(len(a), len(b)) bytes and allocates nothing. Use AndNotInto to write the result elsewhere.

func AndNotInto

func AndNotInto(dst, a, b []byte)

AndNotInto sets dst[i] = a[i] &^ b[i], clearing in a every bit set in b.

It processes min(len(dst), len(a), len(b)) bytes. dst may alias a or b.

func Any

func Any(m []bool) bool

Any reports whether any element is true. An empty mask is false.

func AnyNaN

func AnyNaN[T Float](a []T, mask []bool) bool

AnyNaN reports whether a contains a NaN.

This is the cheap question and it has a cheaper answer than counting: a single reduction that stops at the first one. It still needs the mask, because the comparison and the reduction are separate kernels.

func AppendEscapeJSON

func AppendEscapeJSON[S Text](dst []byte, s S) []byte

AppendEscapeJSON appends s to dst with JSON string escaping applied — the body of a JSON string, without the surrounding quotes — and returns the extended slice.

buf := make([]byte, 0, 4096)          // once
for _, rec := range records {
    buf = append(buf[:0], '"')
    buf = simd.AppendEscapeJSON(buf, rec.Name)
    buf = append(buf, '"')
    ...
}

Escapes are the standard ones: \" \\ \b \f \n \r \t, and \u00XX for the other control bytes. Bytes 0x80 and above pass through, so valid UTF-8 in gives valid UTF-8 out. It does not escape < > & — see the package note; use encoding/json if you are embedding the result in HTML.

Example

JSON string escaping in the append style, so a serializer brings one buffer. NeedsEscapeJSON is one accelerated scan, and on clean input the writer can skip escaping entirely.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	buf := make([]byte, 0, 64)
	buf = append(buf, '"')
	buf = simd.AppendEscapeJSON(buf, "say \"hi\"\n")
	buf = append(buf, '"')
	fmt.Println(string(buf))
	fmt.Println(simd.NeedsEscapeJSON("clean text"))
}
Output:
"say \"hi\"\n"
false

func AppendRunes

func AppendRunes[S Text](dst []rune, s S) []rune

AppendRunes appends the runes of s to dst and returns the extended slice.

It is AppendUTF16's shape for UTF-32, and it exists for the same reason: `[]rune(s)` allocates a fresh slice every call, and a loop over lines or records pays for that once per record. This one appends into a buffer you own, so the same idiom as everywhere else here applies:

runes = simd.AppendRunes(runes[:0], line)

The general UTF-8 decode is a dependent scan — a rune's length decides where the next one starts — so only the ASCII runs are accelerated. Below 0x80 a byte is a whole rune, which makes that case a plain widen with no dependence between lanes, and in real text it is nearly all of the input. The runes between runs are decoded one at a time.

Invalid UTF-8 becomes utf8.RuneError, one per offending byte, matching `[]rune(s)` and utf8.DecodeRune.

func AppendUTF8

func AppendUTF8(dst []byte, s []uint16) []byte

AppendUTF8 appends the UTF-8 encoding of the UTF-16 units in s to dst and returns the extended slice.

Unpaired surrogates become U+FFFD, matching unicode/utf16.Decode.

func AppendUTF8FromRunes

func AppendUTF8FromRunes(dst []byte, s []rune) []byte

AppendUTF8FromRunes encodes the runes of s as UTF-8 and appends them to dst, returning the extended slice.

It is the direction AppendRunes does not go, and completes the UTF-32 pair the way AppendUTF8 completes the UTF-16 one. The alternative is `append(dst, string(runes)...)`, which allocates a string per call for no reason other than that Go has no other spelling for it.

Only the ASCII runs are accelerated, and unlike the decode direction that is not because of a dependence: encoding is independent per rune, but a rune below 0x80 is one byte and a rune above it is two to four, so the output offset depends on every rune before it. Below 0x80 that question disappears — one rune is one byte — which makes the run a plain narrow.

A surrogate half or a value above utf8.MaxRune is encoded as utf8.RuneError, matching utf8.AppendRune and `string(runes)`.

func AppendUTF16

func AppendUTF16[S Text](dst []uint16, s S) []uint16

AppendUTF16 appends the UTF-16 encoding of s to dst and returns the extended slice.

Invalid UTF-8 is handled as encoding/utf8 and unicode/utf16 handle it: each malformed byte becomes U+FFFD, one unit, which is what utf16.Encode of a []rune conversion produces for the same input. Runes above the BMP become surrogate pairs, so the result is longer than the rune count.

func AppendVarints

func AppendVarints[T VarintValue](dst []byte, a []T) []byte

AppendVarints encodes every element of a and appends the result to dst.

The buffer is grown once, to exactly the size VarintSize reports, and the bytes are then written without any further bounds growth. The emission itself is the serial loop it has to be; what the vectorized pass buys is that it happens exactly once into exactly the right amount of memory.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	buf := simd.AppendVarints(nil, []uint64{1, 300})
	fmt.Printf("% x\n", buf)
}
Output:
01 ac 02

func ApplyWindowInto

func ApplyWindowInto[T Float](dst, a, w []T)

ApplyWindowInto multiplies a by a window into dst, which is what a caller actually does with one:

w := make([]float64, n)
simd.Hann(w)
simd.ApplyWindowInto(dst, samples, w)

It is MulInto under a clearer name, and exists so the pairing is discoverable from the window functions.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	w := make([]float64, 4)
	simd.Hann(w)

	dst := make([]float64, 4)
	simd.ApplyWindowInto(dst, []float64{1, 1, 1, 1}, w)
	fmt.Printf("%.2f %.2f\n", dst[0], dst[1])
}
Output:
0.00 0.75

func ArgMax

func ArgMax[T Number](a []T) int

ArgMax returns the index of the first largest element. It panics on an empty slice.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.ArgMax([]float64{3, 1, 4, 1, 5}))
}
Output:
4

func ArgMin

func ArgMin[T Number](a []T) int

ArgMin returns the index of the first smallest element. It panics on an empty slice.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.ArgMin([]float64{3, 1, 4, 1, 5}))
}
Output:
1

func Argsort

func Argsort[T Number](idx []int32, a []T) int

Argsort writes into idx a permutation of 0..len(a)-1 that would sort a in ascending order, leaving a untouched.

This is the operation Go has no good answer for: sorting one slice by the values of another usually means building a slice of structs or a slice of indices closed over the data, and both allocate and both are slow. Here idx is yours and nothing else is allocated.

It writes min(len(idx), len(a)) entries and returns that count. Ties keep their original relative order, so the permutation is stable. NaN sorts to the end, as in Sort.

Applying the result is GatherInto:

n := simd.Argsort(idx, a)
simd.GatherInto(sorted, a, idx[:n])
Example

Argsort returns the permutation rather than reordering, which is what you want when several columns share one ordering.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	names := []string{"cherry", "apple", "banana"}
	score := []float64{3, 1, 2}

	order := make([]int32, len(score))
	simd.Argsort(order, score)
	for _, i := range order {
		fmt.Print(names[i], " ")
	}
	fmt.Println()
}
Output:
apple banana cherry

func Asin

func Asin[T Float](a []T)

Asin replaces every element with its arcsine, in radians. Inputs outside [-1, 1] yield NaN.

func AsinInto

func AsinInto[T Float](dst, a []T)

AsinInto sets dst[i] to the arcsine of a[i]. dst may alias a.

func Asinh

func Asinh[T Float](a []T)

Asinh replaces every element with its inverse hyperbolic sine. It is defined for every finite input.

func AsinhInto

func AsinhInto[T Float](dst, a []T)

AsinhInto sets dst[i] to the inverse hyperbolic sine of a[i]. dst may alias a.

func Atan

func Atan[T Float](a []T)

Atan replaces every element with its arctangent, in radians.

func Atan2

func Atan2[T Float](a, b []T)

Atan2 replaces each element of a with the angle, in radians, of the point (b[i], a[i]) — that is, atan(a[i]/b[i]) resolved to the correct quadrant.

The argument order follows math.Atan2: y first, then x.

func Atan2Into

func Atan2Into[T Float](dst, a, b []T)

Atan2Into sets dst[i] to the angle of the point (b[i], a[i]) in radians.

func AtanInto

func AtanInto[T Float](dst, a []T)

AtanInto sets dst[i] to the arctangent of a[i]. dst may alias a.

func Atanh

func Atanh[T Float](a []T)

Atanh replaces every element with its inverse hyperbolic tangent. It is the signed infinity at exactly -1 and 1, and NaN outside [-1, 1], as C99 specifies.

func AtanhInto

func AtanhInto[T Float](dst, a []T)

AtanhInto sets dst[i] to the inverse hyperbolic tangent of a[i]: the signed infinity at exactly -1 and 1, NaN outside [-1, 1]. dst may alias a.

func AvailableTiers

func AvailableTiers() []string

AvailableTiers returns the names of every instruction-set tier this CPU can execute, weakest first, always beginning with "scalar".

Use it to drive a benchmark or test matrix over the tiers a machine actually supports, rather than a hardcoded list that would be wrong elsewhere:

for _, t := range simd.AvailableTiers() {
    exec.Command(...).Env = append(os.Environ(), "GOSIMD="+t)
}

Tiers masked out by SIMD_DISABLE are excluded.

func BFloat16ToFloat32Into

func BFloat16ToFloat32Into(dst []float32, a []uint16)

BFloat16ToFloat32Into widens each bfloat16 in a into a float32 in dst.

Exact: every bfloat16 is a float32 with its low mantissa bits zero, so nothing is approximated, including NaN payloads and both infinities.

func Bartlett

func Bartlett[T Float](dst []T)

Bartlett fills dst with a symmetric Bartlett (triangular) window.

This one is not a raised cosine, so it is a ramp and an absolute value rather than a transcendental — much the cheapest of the set.

func Base64Decode

func Base64Decode[S Text](dst []byte, src S) int

Base64Decode writes the decoded bytes of src into dst and returns how many it wrote, or -1 if src is not valid standard base64 or dst is too short.

It matches base64.StdEncoding.Decode, except in how it reports a problem: one number rather than a count and an error, because returning an error would allocate.

Validation is not a separate pass. Every character's value is folded together as it is decoded and one bit says whether anything was outside the alphabet, so a rejected input costs the same as an accepted one rather than a branch per character.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]byte, simd.Base64DecodedLen(len("aGVsbG8=")))
	n := simd.Base64Decode(dst, "aGVsbG8=")
	fmt.Println(string(dst[:n]))
}
Output:
hello

func Base64DecodedLen

func Base64DecodedLen(n int) int

Base64DecodedLen is the largest length Base64Decode can write for n input bytes, matching base64.StdEncoding.DecodedLen. The actual count is smaller when the input is padded, and is what Base64Decode returns.

func Base64Encode

func Base64Encode[S Text](dst []byte, src S) int

Base64Encode writes the standard base64 encoding of src into dst and returns how many bytes it wrote, or -1 if dst is shorter than Base64EncodedLen.

It matches base64.StdEncoding: the RFC 4648 alphabet, with padding. Nothing is allocated — which is the difference from encoding/base64's EncodeToString and the reason the length is the caller's to provide.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	src := []byte("any carnal pleasure")

	dst := make([]byte, simd.Base64EncodedLen(len(src)))
	n := simd.Base64Encode(dst, src)
	fmt.Println(string(dst[:n]))

	// Decode returns the number of bytes written, or -1 if the input is not
	// valid base64 — there is no error value to ignore by accident.
	back := make([]byte, simd.Base64DecodedLen(n))
	m := simd.Base64Decode(back, dst[:n])
	fmt.Println(string(back[:m]))

}
Output:
YW55IGNhcm5hbCBwbGVhc3VyZQ==
any carnal pleasure

func Base64EncodedLen

func Base64EncodedLen(n int) int

Base64EncodedLen is the length Base64Encode needs in dst for n input bytes, matching base64.StdEncoding.EncodedLen.

func Bincount

func Bincount[T Integer](a []T, n int) []int32

Bincount is BincountInto allocating the counts. n is the number of bins.

Example

Bincount is the histogram of small integers: counts[v]++ for each v, skipping anything out of range rather than panicking.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dice := []int32{3, 6, 3, 1, 6, 6, 2, 3}
	fmt.Println(simd.Bincount(dice, 7)[1:]) // index 0 unused for a die
}
Output:
[1 1 3 0 0 3]

func BincountInto

func BincountInto[T Integer](counts []int32, a []T)

BincountInto counts occurrences of each value in a, adding the count of value v to counts[v]. Values outside [0, len(counts)) are skipped.

counts is not zeroed first, so repeated calls accumulate. Clear it yourself if that is not what you want:

clear(counts)
simd.BincountInto(counts, a)

func BitPackInto

func BitPackInto(dst, a []uint32, bits int32)

BitPackInto packs the low `bits` bits of each value in a into a dense bitstream in dst, least significant bit first and with no padding.

This is the representation Parquet, Arrow and Lucene use for an integer column after delta encoding: once the deltas are small, storing each in a full 32 bits is mostly zeroes. Pair it with DiffInto to produce the deltas and ZigzagEncodeInt32Into if they can be negative.

dst must have room for ceil(len(a)*bits/32) words. bits must be 1 to 32. It does nothing if either is not so, and it allocates nothing.

Example

The columnar compression pipeline, in the order the pieces are meant to be used: differences first, then zigzag so small negatives stay small, then pack to the width that actually fits.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []uint32{1, 2, 3, 4, 5, 6, 7, 0}

	// Three bits hold 0..7, so eight values need 24 bits — one 32-bit word.
	// Unpacking needs one word MORE than packing: a value whose bits straddle a
	// word boundary reads the next word, and the last one straddles whenever
	// the total is not a multiple of 32. Size for the reader.
	packed := make([]uint32, (len(a)*3+31)/32+1)
	simd.BitPackInto(packed, a, 3)

	back := make([]uint32, len(a))
	simd.BitUnpackInto(back, packed, 3)
	fmt.Println(back)
}
Output:
[1 2 3 4 5 6 7 0]

func BitUnpackInto

func BitUnpackInto(dst, a []uint32, bits int32)

BitUnpackInto is the inverse: it reads len(dst) values of `bits` bits each from the bitstream in a.

The number of values is taken from len(dst), because a bitstream does not record how many values it holds — the caller knows, and a packed block in any real format carries the count beside it.

a must hold ceil(len(dst)*bits/32)+1 words. The extra word is not slack: a value whose bits straddle a word boundary reads the next word, and the last value straddles whenever len(dst)*bits is not a multiple of 32. Requiring it in the guard is what lets the kernel read unconditionally rather than branching on every element.

func Blackman

func Blackman[T Float](dst []T)

Blackman fills dst with a symmetric Blackman window.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	w := make([]float64, 3)
	simd.Blackman(w)
	fmt.Printf("%.4f %.4f %.4f\n", w[0], w[1], w[2])
}
Output:
-0.0000 1.0000 -0.0000

func BottomK

func BottomK[T Number](a []T, k int) []T

BottomK is BottomKInto allocating both the destination and the scratch.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.BottomK([]float64{5, 1, 4, 2, 3}, 3))
}
Output:
[1 2 3]

func BottomKInto

func BottomKInto[T Number](dst []T, a []T, k int, scratch []T) int

BottomKInto is TopKInto for the k smallest, in ascending order.

func ByteSwapInto

func ByteSwapInto[T Integer](dst, a []T)

ByteSwapInto writes each element of a with its bytes in reverse order to dst, which is the byte-order conversion a network or file format needs over a whole slice.

It does nothing for the eight-bit types, where a byte is its own reversal and there is no kernel.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]uint32, 1)
	simd.ByteSwapInto(dst, []uint32{0x11223344})
	fmt.Printf("%#08x\n", dst[0])
}
Output:
0x44332211

func Cbrt

func Cbrt[T Float](a []T)

Cbrt replaces every element with its cube root. Negative inputs are fine.

func CbrtInto

func CbrtInto[T Float](dst, a []T)

CbrtInto sets dst[i] to the cube root of a[i]. dst may alias a.

func Ceil

func Ceil[T Float](a []T)

Ceil rounds every element up, towards positive infinity.

func CeilInto

func CeilInto[T Float](dst, a []T)

CeilInto sets dst[i] to a[i] rounded up. dst may alias a.

func Clamp

func Clamp[T Number](a []T, lo, hi T)

Clamp limits every element to the range [lo, hi].

For floats, NaN inputs propagate rather than being clamped.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{-5, 0.5, 99}
	simd.Clamp(a, 0, 1)
	fmt.Println(a)
}
Output:
[0 0.5 1]

func ClampInto

func ClampInto[T Number](dst, a []T, lo, hi T)

ClampInto sets dst[i] to a[i] limited to [lo, hi]. dst may alias a.

func CommonPrefixLen

func CommonPrefixLen[S, T Text](a S, b T) int

CommonPrefixLen returns how many leading bytes a and b share, at most the length of the shorter.

This is Compare without the ordering, and it is the operation suffix-array construction, trie descent and the LCP array spend their time in. A byte-at-a-time loop pays a compare and a branch for every byte the two share; this reduces sixty-four at a time to a single "did anything differ", which is the case worth vectorizing precisely because a long shared prefix is what these callers usually have.

Bytes, not runes: the answer may land in the middle of a UTF-8 sequence. A caller that needs a rune boundary should back up to one, which is a constant-time step from any byte index.

Example

CommonPrefixLen is Compare without the ordering: how far two slices agree. It is the inner loop of suffix-array construction and trie descent.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.CommonPrefixLen("/usr/local/bin", "/usr/local/lib"))
}
Output:
11

func Compare

func Compare[S, T Text](a S, b T) int

Compare returns -1, 0 or +1 ordering a before, equal to, or after b, lexicographically by content and then by length.

It matches bytes.Compare.

func CompressInto

func CompressInto[T Number](dst, src []T, mask []bool) int

CompressInto writes the elements of src whose mask entry is true into dst, in order, and returns how many it wrote.

dst is not resized; the return value is the meaningful length, so the usual call is followed by a reslice to it. If dst is too short to hold every match the result is truncated rather than a panic, which is deliberate — dst is normally sized from an estimate of how many will match, and an estimate that comes in low should cost a short answer the caller can detect, not a crash.

The mask is read as far as the shorter of src and mask.

This is accelerated only on AVX-512 and SVE2, and that is not a gap waiting to be filled. Where dst[k] goes depends on how many earlier elements matched, so the iterations are genuinely serial and no compiler on any target vectorizes the loop. The two instruction sets that have a compress instruction break the dependency in hardware; the other seven run the portable loop, which is the same loop their compilers would have produced.

Example

Filtering is two steps on purpose: a comparison writes the mask, and CompressInto packs the elements that passed. Splitting it that way is what lets one Compress serve every predicate, including ones this library has never heard of, instead of shipping GreaterThanFilter and the rest.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{-3, 7, -1, 4, 0, 9}

	mask := make([]bool, len(a))
	simd.GreaterScalarInto(mask, a, 0) // which elements pass

	out := make([]float64, len(a))
	n := simd.CompressInto(out, a, mask) // pack the ones that did

	fmt.Println(out[:n])

}
Output:
[7 4 9]

func ConjComplex

func ConjComplex[C Complex](a []C)

ConjComplex replaces each element with its complex conjugate.

func ConjComplexInto

func ConjComplexInto[C Complex](dst, a []C)

ConjComplexInto sets dst[i] to the conjugate of a[i]. dst may alias a.

func Contains

func Contains[S, T Text](haystack S, needle T) bool

Contains reports whether needle is within haystack.

func ContainsAny

func ContainsAny[S, T Text](s S, chars T) bool

ContainsAny reports whether any byte of chars is within s.

func ContainsByte

func ContainsByte[S Text](s S, c byte) bool

ContainsByte reports whether c is within s.

func ContainsFoldASCII

func ContainsFoldASCII[S, T Text](haystack S, needle T, scratch []byte) bool

ContainsFoldASCII reports whether needle occurs in haystack under ASCII case folding. It is IndexFoldASCII >= 0.

func ConvertInto

func ConvertInto[D, S Number](dst []D, src []S)

ConvertInto converts each element of src to the destination element type and writes it to dst, processing min(len(dst), len(src)) elements.

It covers every pair among float32, float64, int32 and int64:

simd.ConvertInto(f64, f32)   // widen
simd.ConvertInto(f32, f64)   // narrow, rounding to nearest even
simd.ConvertInto(i32, f64)   // truncate toward zero
simd.ConvertInto(f64, i64)   // widen, rounding if beyond 2^53

The conversions follow Go's own rules, which is to say the hardware's: float to integer truncates toward zero, and the result is implementation-defined if the value does not fit — so range-check first with Clamp if the input is untrusted.

This is a single generic loop rather than a per-pair kernel. Widening and narowing conversions are single vector instructions (VCVTPS2PD and friends) and will get their own kernels; the signature will not change when they do.

func ConvolveFull

func ConvolveFull[T Float](a, b []T) []T

ConvolveFull is ConvolveFullInto allocating the destination.

Example

An FIR filter is a convolution, which is why this package has no dedicated FIR function. The taps are the kernel; ConvolveFullInto picks direct or frequency-domain by a measured crossover. Here a 4-tap moving average smooths a step — the quarters are exact in float64, so the output is too.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	signal := []float64{0, 0, 0, 4, 4, 4, 4, 4}
	taps := []float64{0.25, 0.25, 0.25, 0.25}

	full := simd.ConvolveFull(signal, taps)
	// For a causal filter, output[i] depends on samples up to i, which is
	// exactly the first len(signal) entries of the full convolution.
	fmt.Println(full[:len(signal)])
}
Output:
[0 0 0 1 2 3 4 4]

func ConvolveFullInto

func ConvolveFullInto[T Float](dst, a, b []T)

ConvolveFullInto writes the full linear convolution of a and b to dst, which must be at least len(a)+len(b)-1 long.

It does nothing if dst is too short, if either input is empty, or if dst overlaps either input.

The algorithm is chosen by the shorter input's length: direct below convCutoff taps and frequency-domain above it, for the reason given there.

Example

ConvolveFullInto picks a direct or FFT convolution by a measured crossover, so the caller does not have to know where it is.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3}
	b := []float64{1, 1}

	dst := make([]float64, len(a)+len(b)-1)
	simd.ConvolveFullInto(dst, a, b)
	fmt.Println(dst)
}
Output:
[1 3 5 3]

func ConvolveInto

func ConvolveInto[T Number](dst, sig, ker []T)

ConvolveInto writes the discrete convolution of sig with ker.

It produces len(sig)-len(ker)+1 elements: the region where the kernel fully overlaps the signal, with no edge padding. Size dst accordingly, or slice it.

func CorrelateFull

func CorrelateFull[T Float](a, b []T) []T

CorrelateFull is CorrelateFullInto allocating both the destination and the scratch.

func CorrelateFullInto

func CorrelateFullInto[T Float](dst, a, b, scratch []T)

CorrelateFullInto writes the full cross-correlation of a and b to dst, which must be at least len(a)+len(b)-1 long.

Correlation is convolution with one operand reversed, so this reverses b into scratch and convolves. scratch must be at least len(b) long; a shorter one is replaced by an allocation.

dst[k] is the overlap at lag k-(len(b)-1), so the zero lag is at index len(b)-1 — the same indexing numpy.correlate uses in "full" mode.

func CorrelateInto

func CorrelateInto[T Number](dst, sig, ker []T)

CorrelateInto writes the cross-correlation of sig with ker.

This is ConvolveInto without reversing the kernel, which is what you want for template matching and for filters whose taps are already in signal order. It also produces len(sig)-len(ker)+1 elements.

func Correlation

func Correlation[T Float](x, y []T) T

Correlation returns the Pearson correlation coefficient of x and y, in [-1, 1]. It returns zero if either input has no variation, since the coefficient is undefined there.

func Cos

func Cos[T Float](a []T)

Cos replaces every element with its cosine, in radians.

func CosInto

func CosInto[T Float](dst, a []T)

CosInto sets dst[i] to the cosine of a[i]. dst may alias a.

func Cosh

func Cosh[T Float](a []T)

Cosh replaces every element with its hyperbolic cosine.

func CoshInto

func CoshInto[T Float](dst, a []T)

CoshInto sets dst[i] to the hyperbolic cosine of a[i]. dst may alias a.

func CosineSimilarity

func CosineSimilarity[T Float](a, b []T) T

CosineSimilarity returns the cosine of the angle between a and b: their dot product divided by the product of their lengths.

The result lies in [-1, 1], where 1 means the vectors point the same way. If either vector is all zeros the angle is undefined and the result is 0.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.CosineSimilarity([]float64{1, 0}, []float64{0, 1}))
}
Output:
0

func Count

func Count[S, T Text](haystack S, needle T) int

Count returns the number of non-overlapping occurrences of needle in haystack.

It matches bytes.Count and strings.Count, including for the empty needle, which counts the runes of haystack plus one. That case is answered here rather than in a kernel, because it is a question about UTF-8 rather than about bytes.

func CountAny

func CountAny[S, T Text](s S, chars T) int

CountAny returns how many bytes of s are in chars.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.CountAny("a,b;c,d", ",;"))
}
Output:
3

func CountByte

func CountByte[S Text](s S, c byte) int

CountByte returns the number of occurrences of c in s.

func CountFoldASCII

func CountFoldASCII[S, T Text](haystack S, needle T, scratch []byte) int

CountFoldASCII counts non-overlapping occurrences of needle in haystack under ASCII case folding, matching bytes.Count's overlap rule.

An empty needle returns the rune-independent answer len(haystack)+1, as bytes.Count does for an empty separator on ASCII input.

func CountNaN

func CountNaN[T Float](a []T, mask []bool) int

CountNaN reports how many elements of a are NaN.

mask is working space of at least len(a); a shorter one is replaced by an allocation, so pass one in a loop.

Example

The mask is supplied rather than allocated, so the whole NaN family runs without touching the heap.

package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, math.NaN(), 3, math.NaN()}
	fmt.Println(simd.CountNaN(a, make([]bool, len(a))))
}
Output:
2

func CountTrue

func CountTrue(m []bool) int

CountTrue returns how many elements are true.

func Covariance

func Covariance[T Float](x, y []T) T

Covariance returns the population covariance of x and y over min(len(x), len(y)) elements, or zero for fewer than two.

func CumMax

func CumMax[T Number](a []T)

CumMax replaces every element with the largest value seen so far.

func CumMaxInto

func CumMaxInto[T Number](dst, a []T)

CumMaxInto writes the running maximum of a into dst. dst may alias a.

func CumMin

func CumMin[T Number](a []T)

CumMin replaces every element with the smallest value seen so far.

func CumMinInto

func CumMinInto[T Number](dst, a []T)

CumMinInto writes the running minimum of a into dst. dst may alias a.

func CumProd

func CumProd[T Number](a []T)

CumProd replaces every element with the running product up to and including it.

Accelerated for int32 and portable everywhere else, which is the one place in this family where the two halves of the rule pull apart. Two's-complement multiplication is associative — wrapping does not change that, since the arithmetic is exact in Z/2^32 — so the blocked scan computes bit for bit what the serial loop does, and a three-cycle multiply leaves enough latency to be worth hiding: 2.04x, verified over four million deliberately overflowing values.

int64 is portable because there is no 64-bit vector multiply below AVX-512DQ (0.67x), and float is portable for the reason CumSum gives. FastCumProd is the opt-in float version.

func CumProdInto

func CumProdInto[T Number](dst, a []T)

CumProdInto writes the running products of a into dst. dst may alias a.

func CumSum

func CumSum[T Number](a []T)

CumSum replaces every element with the running total up to and including it.

This one is permanently portable, and that is a decision rather than a gap

There is no accelerated CumSum on any architecture and there will not be. Each output depends on the one before it, so the only way to vectorize a scan is to regroup the arithmetic — and unlike a reduction, every partial result is written to dst, so the regrouping is visible in the answer. This package's contract is that the accelerated and portable paths agree bit for bit, which forbids exactly that.

For integers the contract is not the obstacle, since integer addition is associative; the measurement is. A blocked scan replaces a chain of dependent adds with a shuffle chain, which only pays when the serial chain is latency-bound, and a one-cycle add is not: int32 measured 980µs serial against 1082µs blocked, and int64 1441µs against 2165µs.

FastCumSum is the opt-in float version, which drops agreement with a naive serial loop — and is, measurably, closer to the true sum. CumMin and CumMax are accelerated for integers, because minimum and maximum are associative and regrouping them changes nothing at all.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3, 4}
	simd.CumSum(a)
	fmt.Println(a)
}
Output:
[1 3 6 10]

func CumSumInto

func CumSumInto[T Number](dst, a []T)

CumSumInto writes the running totals of a into dst. dst may alias a.

func DequantizeInt8

func DequantizeInt8(dst []float32, a []int8, scale float32, zeroPoint int32)

DequantizeInt8 is the inverse: x = (q - zeroPoint) * scale.

func DequantizePerChannelInt8

func DequantizePerChannelInt8(dst []float32, a []int8, scale []float32, zeroPoint []int32, channels, inner int)

DequantizePerChannelInt8 is the inverse of QuantizePerChannelInt8.

func DequantizePerChannelUint8

func DequantizePerChannelUint8(dst []float32, a []uint8, scale []float32, zeroPoint []int32, channels, inner int)

DequantizePerChannelUint8 is the inverse of QuantizePerChannelUint8.

func DequantizeUint8

func DequantizeUint8(dst []float32, a []uint8, scale float32, zeroPoint int32)

DequantizeUint8 is the inverse of QuantizeUint8.

func Describe

func Describe() string

Describe returns a one-line summary of instruction-set selection: the architecture, the chosen tier, every tier the CPU supports, and whether GOSIMD or SIMD_DISABLE altered the outcome.

It is intended for logs and bug reports.

func DiffInto

func DiffInto[T Number](dst, a []T)

DiffInto writes the successive differences of a into dst: dst[i] = a[i+1] - a[i].

It produces one fewer element than a has, so size dst accordingly. dst may alias a.

Example

DiffInto writes successive differences, so it produces one fewer element than it consumes.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 3)
	simd.DiffInto(dst, []float64{1, 3, 6, 10})
	fmt.Println(dst)
}
Output:
[2 3 4]

func DifferenceInto

func DifferenceInto[T Integer](dst, a, b []T) int

DifferenceInto writes the elements of a that are not in b to dst, in ascending order, and returns how many there were.

a and b must be sorted ascending with no duplicates. dst must have room for len(a) elements; it panics otherwise, for the reason IntersectInto gives.

Example

DifferenceInto keeps the elements of a that are not in b.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []int32{1, 3, 5, 7, 9}
	b := []int32{3, 4, 5, 6, 9}

	dst := make([]int32, len(a))
	n := simd.DifferenceInto(dst, a, b)
	fmt.Println(dst[:n])
}
Output:
[1 7]

func Distance

func Distance[T Float](a, b []T) T

Distance returns the Euclidean distance between a and b over min(len(a), len(b)) elements.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Distance([]float64{0, 0}, []float64{3, 4}))
}
Output:
5

func Div

func Div[T Float](a, b []T)

Div divides a by b, elementwise: a[i] /= b[i].

Division by zero follows IEEE 754 and yields ±Inf or NaN; it does not panic.

func DivComplex

func DivComplex[C Complex](a, b []C)

DivComplex divides a by b elementwise: a[i] /= b[i].

The quotient is formed by Smith's method, which divides through by the larger component first, so an intermediate cannot overflow for a result that is representable.

func DivComplexInto

func DivComplexInto[C Complex](dst, a, b []C)

DivComplexInto sets dst[i] = a[i] / b[i]. dst may alias a or b.

func DivInto

func DivInto[T Float](dst, a, b []T)

DivInto sets dst[i] = a[i] / b[i]. dst may alias a or b.

func DivScalar

func DivScalar[T Number](a []T, s T)

DivScalar divides every element by s: a[i] /= s.

It really divides rather than multiplying by a precomputed reciprocal, which is slower but exact: 3*(1/5) is 0.6000000000000001 where 3/5 is 0.6. Integer division truncates toward zero and panics on a zero divisor.

func DivScalarInto

func DivScalarInto[T Number](dst, a []T, s T)

DivScalarInto sets dst[i] = a[i] / s. dst may alias a. See DivScalar.

func Dot

func Dot[T Number](a, b []T) T

Dot returns the dot product of a and b over min(len(a), len(b)) elements, or zero if either is empty.

The multiplication and the addition round separately; they are not fused, which matches a plain scalar loop.

func DotComplex

func DotComplex[C Complex](a, b []C) C

DotComplex returns the bilinear product, the sum of a[i]*b[i].

This is not the inner product of a complex vector space; see DotComplexConj for that one. Both are offered because both are wanted and neither is obviously what "dot" should mean.

func DotComplexConj

func DotComplexConj[C Complex](a, b []C) C

DotComplexConj returns the Hermitian inner product, the sum of conj(a[i])*b[i]. This is the one that makes DotComplexConj(a, a) the squared norm, and the one linear algebra usually means.

func EMAInto

func EMAInto[T Number](dst, a []T, alpha T)

EMAInto writes the exponentially weighted moving average of a, where alpha is the weight given to each new sample: dst[i] = alpha*a[i] + (1-alpha)*dst[i-1].

This one is permanently portable, on every architecture, and the reason has nothing to do with any instruction set. Each output depends on the one before it, so the only way to vectorize it is to regroup the arithmetic — and every partial result is written to dst, so the regrouping is visible in the answer. The package's contract is that the accelerated and portable paths agree bit for bit, which forbids exactly that. CumSum and CumProd are portable for the same reason; CumMin and CumMax are not, because minimum and maximum are associative and regrouping them changes nothing.

It is here because it belongs next to MovingAverageInto, not for speed. A FastEMA that truncates the geometric tail and states its error bound would vectorize, and is the shape any future version of this would take.

func Equal

func Equal[S, T Text](a S, b T) bool

Equal reports whether a and b are the same length and hold the same bytes.

It matches bytes.Equal. A nil argument is equivalent to an empty slice.

func EqualFoldASCII

func EqualFoldASCII[S, T Text](a S, b T) bool

EqualFoldASCII reports whether a and b are equal ignoring ASCII case.

Unlike bytes.EqualFold it does not perform Unicode case folding, which makes it both faster and wrong for non-ASCII input — use it for protocol tokens (HTTP headers, keywords, hex digits), not for user-facing text.

Example

Only ASCII letters are folded, which is what makes it safe to run over UTF-8: every continuation byte is 0x80 or above and outside both ranges.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.EqualFoldASCII("Content-Type", "content-type"))
}
Output:
true

func EqualInto

func EqualInto[T Number](dst []bool, a, b []T)

EqualInto writes whether a[i] == b[i]. NaN compares equal to nothing, itself included.

func EqualScalarInto

func EqualScalarInto[T Number](dst []bool, a []T, v T)

EqualScalarInto writes whether a[i] == v.

func Erf

func Erf[T Float](a []T)

Erf is the error function, in place. It carries an absolute error bound of 1.4e-7 rather than a ULP one; see ErfInto.

func ErfInto

func ErfInto[T Float](dst, a []T)

ErfInto writes the error function of each element of a to dst.

The bound is 1.4e-7 ABSOLUTE, measured over [-6, 6], not a ULP bound. That is the right form of claim here: erf is bounded by 1 and its interesting range is where it is O(1), so an absolute bound is what a caller can use. It comes from the Abramowitz and Stegun 7.1.26 rational form, which is a float32-grade approximation — for float64 the answer is accurate to about seven digits, not sixteen.

func Erfc

func Erfc[T Float](a []T)

Erfc is the complementary error function, in place.

This one has no kernel on any architecture and runs Go's math.Erfc, which is correctly rounded. That is deliberate: see ErfcInto.

func ErfcInto

func ErfcInto[T Float](dst, a []T)

ErfcInto writes the complementary error function of each element of a to dst.

This has no kernel and runs Go's math.Erfc on every architecture. The decision is measured, not conservative: erfc's whole purpose is the tail, where it decays to nothing, and the same 1.4e-7 absolute error that is fine for erf is a RELATIVE error of 1.2e-2 at x=6, where erfc is about 2e-17. An operation whose only interesting regime is wrong to one percent is worse than no kernel at all.

If you want the fast approximation over the range where erfc is O(1), it is 1-ErfInto, and writing that yourself makes the trade explicit.

func EulerStep

func EulerStep[T Float](y, dydt []T, t, h T, f Derivative[T])

EulerStep advances y by one step of size h using the forward Euler method.

It is first order, so its error per step is proportional to h². Use it when the derivative is expensive and accuracy is not critical; prefer RK4Step otherwise.

dydt is scratch the caller owns, the same length as y.

func Exp

func Exp[T Float](a []T)

Exp replaces every element with e raised to it.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{0, 1}
	simd.Exp(a)
	fmt.Printf("%.4f %.4f\n", a[0], a[1])
}
Output:
1.0000 2.7183

func Exp2

func Exp2[T Float](a []T)

Exp2 replaces every element with 2 raised to it.

func Exp2Into

func Exp2Into[T Float](dst, a []T)

Exp2Into sets dst[i] = 2**a[i]. dst may alias a.

func ExpInto

func ExpInto[T Float](dst, a []T)

ExpInto sets dst[i] = e**a[i]. dst may alias a.

func ExpandInto

func ExpandInto[T Number](dst, src []T, mask []bool) int

ExpandInto is the inverse of CompressInto: it walks mask, and wherever it is true takes the next element of src. It returns how many elements of src were consumed.

Positions where the mask is false are left untouched, so filling dst with a default first and expanding over it is the way to scatter a packed buffer back into a full-width one.

This one is portable on every architecture, and unlike Compress that is permanent rather than pending. Compression's serial half is the store, which a compress instruction fixes; expansion's is the load, which it does not. Both AVX-512 and SVE2 compile this to the same scalar loop everything else gets, so there is no kernel to ship.

func Expm1

func Expm1[T Float](a []T)

Expm1 replaces every element x with e**x - 1.

Use it instead of Exp followed by subtracting one when x is near zero, where that form loses almost all its significant digits.

func Expm1Into

func Expm1Into[T Float](dst, a []T)

Expm1Into sets dst[i] = e**a[i] - 1, accurately near zero. dst may alias a.

func FFT

func FFT(a []complex128) []complex128

FFT returns the discrete Fourier transform of a, allocating both the plan and the result. len(a) must be a power of two; it returns nil otherwise.

Use NewFFTPlan and FFTInto in a loop — building the plan is the expensive part and it depends only on the length.

func FFTInto

func FFTInto(p *FFTPlan, dst, src []complex128)

FFTInto writes the discrete Fourier transform of src to dst.

dst and src must both be at least p.Len() long and must not overlap. src is not modified.

The sign convention is the usual one for a forward transform, exp(-2*pi*i*k* n/N), matching numpy.fft and gonum. IFFTInto inverts it exactly, including the 1/N scaling.

Example

The plan holds the twiddle factors, so a loop over many transforms of the same size computes them once.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	p := simd.NewFFTPlan(4)

	src := []complex128{1, 1, 1, 1}
	dst := make([]complex128, 4)
	simd.FFTInto(p, dst, src)
	fmt.Println(real(dst[0]), real(dst[1]))
}
Output:
4 0

func FastAcos

func FastAcos[T Float](a []T)

FastAcos replaces every element with its arccosine, to within 3.5 ULP. See Acos for the accurate form.

func FastAcosInto

func FastAcosInto[T Float](dst, a []T)

FastAcosInto writes the result into dst. dst may alias a.

func FastAcosh

func FastAcosh[T Float](a []T)

FastAcosh replaces every element with its inverse hyperbolic cosine, to within 3.5 ULP. It is NaN below 1. See Acosh for the accurate form.

func FastAcoshInto

func FastAcoshInto[T Float](dst, a []T)

FastAcoshInto writes the result into dst. dst may alias a.

func FastAsin

func FastAsin[T Float](a []T)

FastAsin replaces every element with its arcsine, to within 3.5 ULP. See Asin for the accurate form.

func FastAsinInto

func FastAsinInto[T Float](dst, a []T)

FastAsinInto writes the result into dst. dst may alias a.

func FastAsinh

func FastAsinh[T Float](a []T)

FastAsinh replaces every element with its inverse hyperbolic sine, to within 3.5 ULP. See Asinh for the accurate form.

func FastAsinhInto

func FastAsinhInto[T Float](dst, a []T)

FastAsinhInto writes the result into dst. dst may alias a.

func FastAtan

func FastAtan[T Float](a []T)

FastAtan replaces every element with its arctangent, to within 3.5 ULP. See Atan for the accurate form.

func FastAtan2

func FastAtan2[T Float](a, b []T)

FastAtan2 sets a[i] to atan(a[i]/b[i]) in the correct quadrant, to within 3.5 ULP. See Atan2 for the accurate form.

func FastAtan2Into

func FastAtan2Into[T Float](dst, a, b []T)

FastAtan2Into writes the result into dst. dst may alias a or b.

func FastAtanInto

func FastAtanInto[T Float](dst, a []T)

FastAtanInto writes the result into dst. dst may alias a.

func FastAtanh

func FastAtanh[T Float](a []T)

FastAtanh replaces every element with its inverse hyperbolic tangent, to within 3.5 ULP. It is the signed infinity at exactly -1 and 1, and NaN outside [-1, 1]. See Atanh for the accurate form.

func FastAtanhInto

func FastAtanhInto[T Float](dst, a []T)

FastAtanhInto writes the result into dst. dst may alias a.

func FastCbrt

func FastCbrt[T Float](a []T)

FastCbrt replaces every element with its cube root, to within 3.5 ULP. See Cbrt for the accurate form.

func FastCbrtInto

func FastCbrtInto[T Float](dst, a []T)

FastCbrtInto writes the result into dst. dst may alias a.

func FastCos

func FastCos[T Float](a []T)

FastCos replaces every element with its cosine, to within 3.5 ULP. See Cos for the accurate form.

func FastCosInto

func FastCosInto[T Float](dst, a []T)

FastCosInto writes the result into dst. dst may alias a.

func FastCosh

func FastCosh[T Float](a []T)

FastCosh replaces every element with its hyperbolic cosine, to within 3.5 ULP. See Cosh for the accurate form.

func FastCoshInto

func FastCoshInto[T Float](dst, a []T)

FastCoshInto writes the result into dst. dst may alias a.

func FastCumProd

func FastCumProd[T Float](a []T)

FastCumProd is CumProd grouped for the vector unit, trading agreement with a serial loop exactly as FastCumSum does.

Measured at four million elements against CumProd: 3.65x on float32 and 1.76x on float64 — the largest speedup in this family, and unlike FastCumSum it is worth having for both types. A floating-point multiply has the longest serial latency of the four combines, so there is the most to hide behind the shuffles.

Integer CumProd needs no Fast form: two's-complement multiplication is associative, so CumProd on int32 is already this algorithm and already bit-identical to the serial loop, at 2.17x.

func FastCumProdInto

func FastCumProdInto[T Float](dst, a []T)

FastCumProdInto writes the running products of a into dst. dst may alias a.

func FastCumSum

func FastCumSum[T Float](a []T)

FastCumSum is CumSum with the prefix sums grouped for the vector unit.

What is traded

A prefix scan writes every partial result, so its grouping is observable: the serial loop computes ((a0+a1)+a2) where the vector form computes (a0+(a1+a2)), and floating-point addition is not associative, so the two differ in the last place. CumSum therefore stays serial, permanently, and this is the opt-in that does not.

What is NOT traded is agreement between machines. The block is sixteen elements for float32 and eight for float64, on every tier and in the portable path, so this returns identical bits on a Graviton, an AVX-512 box and `-tags purego`. Only agreement with a naive loop is given up.

And it is not less accurate

The obvious reading of Fast* — as it is for the transcendentals, which really do trade 1.0 ULP for 3.5 — is wrong here. Blocked summation has O(log n) error growth where a running accumulator has O(n), so measured against a long-double scan of a million values this is *closer* to the true result than CumSum on every corpus tried: 680,000 of a million elements closer on uniform positive input, and on the case that breaks serial accumulation — 1e16 followed by a million ones, where the running total cannot represent the increment — a mean absolute error of 1.0 against CumSum's 5.0e+05.

float64 is the serial loop, deliberately

Measured at four million elements, against CumSum:

float32   1669 us -> 1347 us   1.24x   on avx512, and 2.59x on sse2
float64   1745 us -> 1909 us   0.91x   SLOWER, so it is not used

Eight doubles fill one AVX-512 register, so the shift steps become cross-lane permutes and there is not enough serial latency to hide behind them. float32 has sixteen lanes and wins. Rather than offer a Fast function that is slower than the plain one on the tier most machines select, FastCumSum on float64 IS CumSum — same bits, same speed, no surprise. The float32 path is the blocked scan and everything above applies to it.

The wider the vector, the less this helps, which is why sse2 is the fastest tier for it. That is unusual enough to be worth stating: the cost of a log-shift scan is the shuffle, and shuffles get more expensive with width while the work per element does not.

func FastCumSumInto

func FastCumSumInto[T Float](dst, a []T)

FastCumSumInto writes the running totals of a into dst. dst may alias a. See FastCumSum for what it trades.

func FastErf

func FastErf[T Float](a []T)

FastErf replaces every element with the error function of that element.

Unlike the rest of this tier it carries an absolute error bound rather than a ULP one, for the reason Erf gives: a rational approximation to erf is accurate in absolute terms and not in relative ones near its zero.

func FastErfInto

func FastErfInto[T Float](dst, a []T)

FastErfInto writes the result into dst. dst may alias a.

func FastExp

func FastExp[T Float](a []T)

FastExp replaces every element with e raised to it, to within 3.5 ULP. See Exp for the accurate form.

func FastExp2

func FastExp2[T Float](a []T)

FastExp2 replaces every element with 2 raised to it, to within 3.5 ULP. See Exp2 for the accurate form.

func FastExp2Into

func FastExp2Into[T Float](dst, a []T)

FastExp2Into writes the result into dst. dst may alias a.

func FastExpInto

func FastExpInto[T Float](dst, a []T)

FastExpInto writes the result into dst. dst may alias a.

func FastExpm1

func FastExpm1[T Float](a []T)

FastExpm1 replaces every element with e raised to it, minus one, to within 3.5 ULP. See Expm1 for the accurate form.

func FastExpm1Into

func FastExpm1Into[T Float](dst, a []T)

FastExpm1Into writes the result into dst. dst may alias a.

func FastHypot

func FastHypot[T Float](a, b []T)

FastHypot sets a[i] to the length of the hypotenuse with sides a[i] and b[i], to within 3.5 ULP. See Hypot for the accurate form.

func FastHypotInto

func FastHypotInto[T Float](dst, a, b []T)

FastHypotInto writes the result into dst. dst may alias a or b.

func FastLog

func FastLog[T Float](a []T)

FastLog replaces every element with its natural logarithm, to within 3.5 ULP. See Log for the accurate form.

func FastLog1p

func FastLog1p[T Float](a []T)

FastLog1p replaces every element with the natural logarithm of one plus it, to within 3.5 ULP. See Log1p for the accurate form.

func FastLog1pInto

func FastLog1pInto[T Float](dst, a []T)

FastLog1pInto writes the result into dst. dst may alias a.

func FastLog2

func FastLog2[T Float](a []T)

FastLog2 replaces every element with its base-2 logarithm, to within 3.5 ULP. See Log2 for the accurate form.

func FastLog2Into

func FastLog2Into[T Float](dst, a []T)

FastLog2Into writes the result into dst. dst may alias a.

func FastLog10

func FastLog10[T Float](a []T)

FastLog10 replaces every element with its base-10 logarithm, to within 3.5 ULP. See Log10 for the accurate form.

func FastLog10Into

func FastLog10Into[T Float](dst, a []T)

FastLog10Into writes the result into dst. dst may alias a.

func FastLogInto

func FastLogInto[T Float](dst, a []T)

FastLogInto writes the result into dst. dst may alias a.

func FastPow

func FastPow[T Float](a, b []T)

FastPow sets a[i] to a[i] raised to the power b[i], to within 3.5 ULP. See Pow for the accurate form.

func FastPowInto

func FastPowInto[T Float](dst, a, b []T)

FastPowInto writes the result into dst. dst may alias a or b.

func FastSigmoid

func FastSigmoid[T Float](a []T)

FastSigmoid replaces every element with its logistic sigmoid, to within 3.5 ULP. See Sigmoid for the accurate form.

func FastSigmoidInto

func FastSigmoidInto[T Float](dst, a []T)

FastSigmoidInto writes the result into dst. dst may alias a.

func FastSin

func FastSin[T Float](a []T)

FastSin replaces every element with its sine, to within 3.5 ULP. See Sin for the accurate form.

func FastSinInto

func FastSinInto[T Float](dst, a []T)

FastSinInto writes the result into dst. dst may alias a.

func FastSinh

func FastSinh[T Float](a []T)

FastSinh replaces every element with its hyperbolic sine, to within 3.5 ULP. See Sinh for the accurate form.

func FastSinhInto

func FastSinhInto[T Float](dst, a []T)

FastSinhInto writes the result into dst. dst may alias a.

func FastTan

func FastTan[T Float](a []T)

FastTan replaces every element with its tangent, to within 3.5 ULP. See Tan for the accurate form.

func FastTanInto

func FastTanInto[T Float](dst, a []T)

FastTanInto writes the result into dst. dst may alias a.

func FastTanh

func FastTanh[T Float](a []T)

FastTanh replaces every element with its hyperbolic tangent, to within 3.5 ULP. See Tanh for the accurate form.

func FastTanhInto

func FastTanhInto[T Float](dst, a []T)

FastTanhInto writes the result into dst. dst may alias a.

func Fill

func Fill[T Number](a []T, v T)

Fill sets every element to v.

func FilterInto

func FilterInto[T Number](dst, src []T, pred func(T) bool) int

FilterInto keeps the elements of src for which pred returns true, writing them into dst and returning how many.

The predicate is a Go closure called once per element, so this is the convenient form rather than the fast one — a call per element defeats the vector unit entirely. It is here because a filter with an arbitrary condition is a real thing to want, and writing it out by hand is worse. When the predicate is a comparison against a constant, build the mask with the vectorized comparison in compare.go and call CompressInto instead; that is the path this function exists to be slower than.

Example

FilterInto takes an arbitrary Go predicate, which is convenient and not fast: the call per element cannot be vectorized. Use a comparison plus CompressInto when it matters.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 5)
	n := simd.FilterInto(dst, []float64{1, -2, 3, -4, 5},
		func(v float64) bool { return v > 0 })
	fmt.Println(dst[:n])
}
Output:
[1 3 5]

func Float8E4M3ToFloat32Into

func Float8E4M3ToFloat32Into(dst []float32, a []byte)

Float8E4M3ToFloat32Into widens OCP e4m3 to float32: 1 sign bit, 4 exponent bits, 3 mantissa bits, bias 7. This is the format weights and activations use.

**e4m3 has no infinity.** Exponent 1111 with mantissa 111 is the only NaN encoding and every other value at that exponent is finite, which is what gives the format its 448 maximum rather than 240. That is the OCP OFP8 definition and NVIDIA's e4m3fn — the "fn" is finite-not-nan — and it is what every shipping implementation does. Compare Float8E5M2ToFloat32Into, which is IEEE-shaped and does have infinities.

It writes min(len(dst), len(a)) elements and allocates nothing.

func Float8E5M2ToFloat32Into

func Float8E5M2ToFloat32Into(dst []float32, a []byte)

Float8E5M2ToFloat32Into widens e5m2 to float32: 1 sign bit, 5 exponent bits, 2 mantissa bits, bias 15. This is the format gradients use, and it trades e4m3's extra mantissa bit for float16's exponent range.

Unlike e4m3 this one is IEEE-shaped: it has infinities and NaNs where a float16 has them.

func Float16ToFloat32Into

func Float16ToFloat32Into(dst []float32, a []uint16)

Float16ToFloat32Into widens each float16 in a into a float32 in dst.

Exact, including denormals, which are renormalized rather than flushed.

Example

float16 and bfloat16 are storage formats: half the bytes, and the conversion is what the vector unit is for.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float32, 2)
	simd.Float16ToFloat32Into(dst, []uint16{0x3c00, 0x4000}) // 1.0, 2.0
	fmt.Println(dst)
}
Output:
[1 2]

func Float32ToBFloat16Into

func Float32ToBFloat16Into(dst []uint16, a []float32)

Float32ToBFloat16Into narrows each float32 in a into a bfloat16 in dst, rounding to nearest even.

Rounding rather than truncating, which costs one add and is the difference between a rounding error and a drift: truncation is biased, and a bias applied to every weight in a network accumulates. A NaN is passed through quieted rather than rounded, because rounding can carry into the exponent and turn a NaN with a low mantissa into an infinity.

func Float32ToFloat8E4M3Into

func Float32ToFloat8E4M3Into(dst []byte, a []float32)

Float32ToFloat8E4M3Into narrows to OCP e4m3, rounding to nearest even.

Because the format has no infinity, values above 448 in magnitude saturate to ±448 rather than becoming one, and an input infinity saturates too. A NaN stays a NaN. Denormals are produced rather than flushed.

Example

e4m3 trades exponent range for mantissa, which is the trade inference weights want. It has no infinity: the largest value is 448.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]byte, 2)
	simd.Float32ToFloat8E4M3Into(dst, []float32{1, 2})

	back := make([]float32, 2)
	simd.Float8E4M3ToFloat32Into(back, dst)
	fmt.Println(back)
}
Output:
[1 2]

func Float32ToFloat8E5M2Into

func Float32ToFloat8E5M2Into(dst []byte, a []float32)

Float32ToFloat8E5M2Into narrows to e5m2, rounding to nearest even.

Values above 57344 in magnitude become infinities, as in float16. Denormals are produced rather than flushed.

func Float32ToFloat16Into

func Float32ToFloat16Into(dst []uint16, a []float32)

Float32ToFloat16Into narrows each float32 in a into a float16 in dst, rounding to nearest even.

Values above 65520 in magnitude become infinities and values below the smallest float16 denormal become zeros, both with the sign preserved. Denormals are produced rather than flushed.

func Floor

func Floor[T Float](a []T)

Floor rounds every element down, towards negative infinity.

func FloorInto

func FloorInto[T Float](dst, a []T)

FloorInto sets dst[i] to a[i] rounded down. dst may alias a.

func FormatInts

func FormatInts(dst []byte, vals []int64, sep byte) int

FormatInts writes vals as decimal text separated by sep — the inverse of ParseInts — and returns how many bytes it wrote, or -1 if dst cannot hold the result.

Size dst at 21 bytes per value — a sign, up to nineteen digits and the separator — and the fast path never needs to measure first:

dst := make([]byte, 21*len(vals))
n := simd.FormatInts(dst, vals, ',')
line := dst[:n]

A tighter dst still works when the rendering actually fits; it just runs the exact-fit reference. -1 means not even the exact rendering fits.

Measured over 200,000 values, both sides reusing their buffers: 957µs against 1678µs for a strconv.AppendInt loop — 1.75x. (The C kernel alone probes at 3.3x; the gap is the call and guard overhead a Go caller actually pays, and the honest number is the one that includes it.) The kernel renders two digits per table lookup, halving both the divisions and the stores. No separator follows the last value.

func FromPartsInto

func FromPartsInto[C Complex, R Float](dst []C, re, im []R)

FromPartsInto assembles dst[i] from re[i] and im[i].

func GELU

func GELU[T Float](a []T)

GELU applies the Gaussian error linear unit, using the tanh approximation that transformer implementations conventionally use:

0.5x * (1 + tanh(sqrt(2/pi) * (x + 0.044715x³)))

The exact form uses the error function; this one is within about 1e-3 of it and is what published model weights were trained against, so it is the compatible choice rather than merely the fast one.

func GatherInto

func GatherInto[T Number](dst, src []T, idx []int32)

GatherInto reads src at each index in idx: dst[i] = src[idx[i]].

Indices outside src are skipped, leaving dst untouched at that position, rather than panicking. A gather is usually driven by computed indices where a stray value should not take the process down; check the indices yourself if you need strictness.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 3)
	simd.GatherInto(dst, []float64{10, 20, 30, 40}, []int32{3, 0, 2})
	fmt.Println(dst)
}
Output:
[40 10 30]

func GemmPackLen

func GemmPackLen[T Float](k, n int) int

GemmPackLen returns the scratch length PackBInto needs for a k-by-n B.

func GemvInto

func GemvInto[T Number](dst, a, x []T, m, k int)

GemvInto multiplies an m*k row-major matrix by a k-vector into m results: dst[i] = sum over p of a[i*k+p] * x[p].

This is the shape most callers actually want — a matrix applied to a vector, once per row — and it is much cheaper than going through MatMulInto with n=1, which would treat the vector as a one-column matrix and give up the contiguous reads that make the reduction fast.

Row i is bit-identical to Dot of that row against x. That is by construction rather than by coincidence, so a caller can freely mix the two.

It does nothing if the slices are too short for the stated dimensions.

// a 1000x256 matrix applied to a 256-vector
simd.GemvInto(dst, a, x, 1000, 256)
Example

GemvInto applies a matrix to a vector, which is the operation most callers actually want and much cheaper than going through MatMulInto with n=1.

Row i of the result is bit-identical to Dot of that row against x — by construction, not by coincidence — so the two can be mixed freely.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{ // 3x2, row-major
		1, 2,
		3, 4,
		5, 6,
	}
	x := []float64{10, 20}

	y := make([]float64, 3)
	simd.GemvInto(y, a, x, 3, 2)

	fmt.Println(y)
	fmt.Println(y[1] == simd.Dot(a[2:4], x))

}
Output:
[50 110 170]
true

func GemvParallelInto added in v1.1.0

func GemvParallelInto[T Number](dst, a, x []T, m, k int)

GemvParallelInto is GemvInto across several goroutines.

dst is length m, a is m×k row-major and x is length k, and the result is bit-identical to GemvInto — the work divides by output row, so no element's summation over k changes order.

The same advice as MatMulParallelInto applies: use it when this product is the whole job, and stay with GemvInto when you are already running work in parallel yourself.

Example

GemvParallelInto is GemvInto across goroutines, for when one matrix-vector product is the whole job. Like MatMulParallelInto it divides by output row, so the result is bit-identical to the serial version, and below a few million multiply-accumulates it runs the serial kernel anyway.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	//  [1 2]   [1]   [ 5]
	//  [3 4] * [2] = [11]
	a := []float64{1, 2, 3, 4} // 2x2, row-major
	x := []float64{1, 2}

	dst := make([]float64, 2)
	simd.GemvParallelInto(dst, a, x, 2, 2)

	fmt.Println(dst)
}
Output:
[5 11]

func GrayscaleInto

func GrayscaleInto(dst, r, g, b []byte)

GrayscaleInto writes the BT.601 luma of three planar colour channels:

Y = 0.299 R + 0.587 G + 0.114 B

The channels are separate slices — one per component — rather than interleaved RGBRGB. That is the layout a vector unit can use, and it is the same struct-of-arrays advice the tutorial gives for everything else here.

The arithmetic is Q8 fixed point, so the result is exact and identical on every instruction set rather than carrying a floating-point error bound. It rounds to nearest; truncating would bias an image dark by half a level.

It writes min of every argument's length and allocates nothing.

Example

Grayscale uses the libjpeg BT.601 weights in Q16, so it agrees with every other implementation of the same conversion to the bit.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]byte, 3)
	simd.GrayscaleInto(dst,
		[]byte{255, 0, 0}, // r
		[]byte{0, 255, 0}, // g
		[]byte{0, 0, 255}) // b
	fmt.Println(dst)
}
Output:
[76 150 29]

func GreaterEqualInto

func GreaterEqualInto[T Number](dst []bool, a, b []T)

GreaterEqualInto writes whether a[i] >= b[i].

func GreaterEqualScalarInto

func GreaterEqualScalarInto[T Number](dst []bool, a []T, v T)

GreaterEqualScalarInto writes whether a[i] >= v.

func GreaterInto

func GreaterInto[T Number](dst []bool, a, b []T)

GreaterInto writes whether a[i] > b[i].

func GreaterScalarInto

func GreaterScalarInto[T Number](dst []bool, a []T, v T)

GreaterScalarInto writes whether a[i] > v.

func Hamming

func Hamming[T Float](dst []T)

Hamming fills dst with a symmetric Hamming window.

Example

Hamming is the window function. The bit-counting operation is HammingDistance — the two are unrelated and the names collide, which is why one of them is spelled out.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	w := make([]float64, 3)
	simd.Hamming(w)
	fmt.Printf("%.2f %.2f %.2f\n", w[0], w[1], w[2])
}
Output:
0.08 1.00 0.08

func HammingDistance

func HammingDistance[S, T Text](a S, b T) int

HammingDistance returns the number of bit positions at which a and b differ, over the shorter of the two.

The name is spelled out because Hamming is already the Hamming *window*, a different thing from the same person: that one shapes a signal before an FFT, this one compares two bit vectors.

This is the fused popcount(a^b). Both halves are separately available here as XorInto and PopCount, and chaining them is the wrong way to do it: that needs a destination buffer the size of the input and three passes over memory where this makes one. At the sizes Hamming distance is used at — binary embedding search, LSH buckets, SimHash near-duplicate detection — the intermediate is most of the cost.

The result is exact and identical on every instruction set. It allocates nothing.

Example

HammingDistance is the number of differing bits, in one pass over both slices rather than an xor pass and a popcount pass.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.HammingDistance([]byte{0b1111_0000}, []byte{0b1010_0000}))
}
Output:
2

func HammingDistanceWords

func HammingDistanceWords(a, b []uint64) int

HammingDistanceWords is HammingDistance for a bit vector already stored as []uint64, which is the layout most binary-embedding indexes use. It gives the same answer as HammingDistance over the same bytes and saves the caller an allocating conversion.

func Hann

func Hann[T Float](dst []T)

Hann fills dst with a symmetric Hann window.

Example

A window is generated once and applied to every frame.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	w := make([]float64, 4)
	simd.Hann(w)
	fmt.Printf("%.2f %.2f %.2f %.2f\n", w[0], w[1], w[2], w[3])
}
Output:
0.00 0.75 0.75 0.00

func HannPeriodic

func HannPeriodic[T Float](dst []T)

HannPeriodic fills dst with a periodic Hann window, which is the symmetric window of length len(dst)+1 with its last sample dropped. This is the form spectral analysis wants; Hann is the form filter design wants.

func HasPrefix

func HasPrefix[S, T Text](s S, prefix T) bool

HasPrefix reports whether s begins with prefix.

func HasSuffix

func HasSuffix[S, T Text](s S, suffix T) bool

HasSuffix reports whether s ends with suffix.

func HexDecode

func HexDecode[S Text](dst []byte, src S) (int, bool)

HexDecode decodes hexadecimal from src into dst, returning the number of bytes written and whether the whole input was valid. Both upper and lower case digits are accepted.

On a bad digit it stops there and reports false, with the bytes decoded so far already written. An odd-length input also reports false.

func HexEncode

func HexEncode[S Text](dst []byte, src S) int

HexEncode writes the lowercase hexadecimal encoding of src into dst and returns the number of bytes written. dst needs room for 2*len(src).

It matches encoding/hex.Encode.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]byte, 8)
	n := simd.HexEncode(dst, []byte{0xde, 0xad, 0xbe, 0xef})
	fmt.Println(string(dst[:n]))
}
Output:
deadbeef

func Hilbert

func Hilbert(src []float64) []complex128

Hilbert returns the analytic signal of src, allocating the plan and the result. len(src) must be a power of two; it returns nil otherwise.

func HilbertInto

func HilbertInto(p *FFTPlan, dst []complex128, src []float64)

HilbertInto writes the analytic signal of the real sequence src to dst: a complex sequence whose real part is src and whose imaginary part is its Hilbert transform.

len(src) must be p.Len(), a power of two, and dst must be at least as long.

The construction is the frequency-domain one, which is why this lives beside the FFT rather than in window.go: transform, discard the negative frequencies and double the positive ones, transform back. The time-domain alternative is convolution with a filter whose ideal impulse response decays as 1/n and so has to be truncated, which is both slower and less accurate.

The direct-current and Nyquist bins are left alone rather than doubled. They have no negative-frequency partner to fold in — bin 0 and bin n/2 are their own conjugates for real input — and doubling them is the classic error, showing up as a constant offset and an alternating ripple.

The envelope of a signal is the magnitude of its analytic signal, which is what most callers want this for:

simd.HilbertInto(p, analytic, samples)
simd.AbsComplexInto(env, analytic)
Example

The analytic signal: the Hilbert transform gives a complex signal whose magnitude is the envelope.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	src := []float64{0, 1, 0, -1}
	p := simd.NewFFTPlan(len(src))

	analytic := make([]complex128, len(src))
	simd.HilbertInto(p, analytic, src)

	env := make([]float64, len(src))
	simd.AbsComplexInto(env, analytic)
	fmt.Printf("%.2f %.2f %.2f %.2f\n", env[0], env[1], env[2], env[3])
}
Output:
1.00 1.00 1.00 1.00

func Histogram

func Histogram[T Number](a []T, n int, lo, hi T) []int32

Histogram is HistogramInto allocating the counts.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// Five values over [0,10) in 2 bins.
	fmt.Println(simd.Histogram([]float64{1, 2, 3, 8, 9}, 2, 0, 10))
}
Output:
[3 2]

func HistogramInto

func HistogramInto[T Number](counts []int32, a []T, lo, hi T)

HistogramInto counts the elements of a falling into len(counts) equal-width bins spanning [lo, hi), adding to counts[i] the number that land in bin i.

Values below lo or at or above hi are skipped, and so is a NaN, which compares false against both bounds. The range is half-open at the top, which is numpy's default and the usual convention, so a value exactly equal to hi is not counted.

counts is not zeroed first, so repeated calls accumulate. It does nothing if hi is not greater than lo.

func Hypot

func Hypot[T Float](a, b []T)

Hypot replaces each element of a with sqrt(a[i]**2 + b[i]**2), computed without the intermediate overflow that squaring would cause.

func HypotInto

func HypotInto[T Float](dst, a, b []T)

HypotInto sets dst[i] = sqrt(a[i]**2 + b[i]**2), avoiding overflow.

func IFFT

func IFFT(a []complex128) []complex128

IFFT returns the inverse transform of a, including the 1/N scaling.

func IFFTInto

func IFFTInto(p *FFTPlan, dst, src []complex128)

IFFTInto writes the inverse discrete Fourier transform of src to dst, including the 1/N scaling, so IFFTInto(FFTInto(x)) recovers x.

func ImagInto

func ImagInto[R Float, C Complex](dst []R, a []C)

ImagInto writes the imaginary component of each element of a into dst.

func Index

func Index[S, T Text](haystack S, needle T) int

Index returns the index of the first occurrence of needle in haystack, or -1. An empty needle returns 0.

It matches bytes.Index and strings.Index and is a drop-in replacement for either.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Index("the quick brown fox", "brown"))
}
Output:
10

func IndexAll

func IndexAll[S Text](dst []int32, s S, c byte) int

IndexAll writes the offset of every occurrence of c in s into dst, and returns how many it found. It stops early if dst fills up, so a short dst bounds the work rather than being an error.

This is the structural-index step of a vectorized parser: run it once for each delimiter class you care about and you have the shape of the document before you have looked at a single byte twice.

offsets := make([]int32, 1024)
n := simd.IndexAll(offsets, line, ',')
for _, off := range offsets[:n] { ... }

Offsets are int32, so an input longer than 2 GiB cannot be indexed past that point; every path here truncates identically rather than one disagreeing with another. Split such an input and add the base yourself.

Example

IndexAll is the structural-index step of a parser: every delimiter located in one pass, then the offsets walked. Like the rest of the text functions it takes a string or a []byte and copies neither.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	line := "id,name,email,created_at"

	commas := make([]int32, 16)
	n := simd.IndexAll(commas, line, ',')

	fields, prev := []string{}, 0
	for _, off := range commas[:n] {
		fields = append(fields, line[prev:off])
		prev = int(off) + 1
	}
	fields = append(fields, line[prev:])

	fmt.Println(n, fields)

}
Output:
3 [id name email created_at]

func IndexAllAny added in v1.3.0

func IndexAllAny[S Text](dst []int32, s S, chars string) int

IndexAllAny writes the offset of every byte in s that equals any of the bytes in chars into dst, and returns how many it found.

This is IndexAll for a set, and it exists because asking for several delimiters one at a time means several passes over the input. A JSON parser wants six at once; doing that as six calls reads the document six times, and this reads it once.

At most eight distinct bytes. A longer set falls back to the portable path, which is correct and not faster — the limit is the packed argument the kernel receives, and eight is what fits beside the pointers.

Like IndexAll it stops when dst fills, so a short dst bounds the work rather than being an error.

Example

IndexAllAny finds several delimiters in one pass. A parser wanting six of them at once would otherwise read the input six times.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	doc := []byte(`{"a":[1,2]}`)

	pos := make([]int32, len(doc))
	n := simd.IndexAllAny(pos, doc, "{}[]:,")

	fmt.Println(pos[:n])
}
Output:
[0 4 5 7 9 10]

func IndexAny

func IndexAny[S, T Text](s S, chars T) int

IndexAny returns the index of the first byte of s that is also in chars, or -1.

The set is turned into a 256-bit table once, so the cost is linear in s and independent of how many characters you are looking for.

Note the difference from strings.IndexAny, which searches for Unicode code points: this is a byte scan. For an ASCII set the two agree, because no byte of a multi-byte UTF-8 sequence is below 0x80. For a set containing non-ASCII characters they do not, and this is the wrong function.

Example

IndexAny finds the first byte belonging to a set, which is the shape a tokenizer wants.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.IndexAny("key=value;next", "=;"))
}
Output:
3

func IndexAnyOrLess added in v1.7.0

func IndexAnyOrLess[S, T Text](s S, chars T, lo byte) int

IndexAnyOrLess returns the index of the first byte of s that is either in chars or numerically below lo, or -1 if no byte is either.

This is IndexAny with a threshold folded in, and it exists because the question callers actually have is usually both at once: find the next byte that is not ordinary text — a delimiter, a quote, a backslash, or any control character. Asking separately means two scans over the same bytes and then taking the smaller answer, which costs twice as much and, on a string that answers immediately, twice as much of nothing.

// The inner loop of a JSON string encoder: copy up to the next byte that
// has to be rewritten, and start again after it.
n := simd.IndexAnyOrLess(s, `"\`, 0x20)

lo of 0 excludes nothing, since no byte is below it, and the call is then exactly IndexAny.

Example

IndexAnyOrLess adds a threshold to that set, which is what an encoder wants: the next byte that has to be rewritten is either punctuation it cares about or any control character at all.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.IndexAnyOrLess("plain text\tthen a tab", `"\\`, 0x20))
}
Output:
10

func IndexByte

func IndexByte[S Text](s S, c byte) int

IndexByte returns the index of the first occurrence of c in s, or -1.

It matches bytes.IndexByte and strings.IndexByte and is a drop-in replacement for either.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.IndexByte("a,b,c", ','))
}
Output:
1

func IndexFoldASCII

func IndexFoldASCII[S, T Text](haystack S, needle T, scratch []byte) int

IndexFoldASCII returns the index of the first occurrence of needle in haystack under ASCII case folding, or -1 if it is not present.

scratch must be at least len(haystack); a shorter one is replaced by an allocation. Give it len(haystack)+len(needle) and the call allocates nothing at all — the needle's fold is carved from the same scratch. (A stack buffer for the needle sounds cheaper and is not: it escapes through the dispatch call and heap-allocates every time, which the allocation test caught.)

scratch := make([]byte, len(page))     // once
for _, w := range words {
    if simd.IndexFoldASCII(page, w, scratch) >= 0 { ... }
}

The returned index is a position in the original haystack — folding does not move bytes, so offsets in the folded copy are offsets in the input.

Example

Case-insensitive search without allocating lowered copies: the haystack is folded once into caller scratch at ToLowerASCII speed, and offsets in the fold are offsets in the original.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	page := []byte("The QUICK brown fox")
	scratch := make([]byte, len(page))
	fmt.Println(simd.IndexFoldASCII(page, "quick", scratch))
	fmt.Println(simd.ContainsFoldASCII(page, "BROWN FOX", scratch))
}
Output:
4
true

func IndexNotAny

func IndexNotAny[S, T Text](s S, chars T) int

IndexNotAny returns the index of the first byte of s that is *not* in chars, or -1 if every byte is.

This is the primitive under trimming and under skipping a run of whitespace, which is where a tokenizer spends the time it is not spending in IndexAny. An empty set contains nothing, so every byte is outside it and the answer for a non-empty s is 0.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.IndexNotAny("   indented", " "))
}
Output:
3

func Interp

func Interp[T Float](x, xp, fp []T) []T

Interp is InterpInto allocating the destination.

func InterpInto

func InterpInto[T Float](dst []T, x, xp, fp []T)

InterpInto writes to dst the piecewise-linear interpolation of (xp, fp) evaluated at each x, matching numpy's interp.

xp must be increasing; the result is undefined if it is not, and that is not checked, because checking costs a pass over xp on every call and the caller almost always knows. Values of x below xp[0] give fp[0] and values above the last xp give the last fp, which is numpy's clamping default rather than extrapolation.

The search for each x is a binary search over xp, which is where the time goes and is not vectorisable — the branch depends on the loaded value. What is vectorisable is the interpolation itself, and it is left as a plain expression because the search dominates: at 64 knots the search is six dependent loads against three arithmetic operations.

Example

InterpInto is numpy's interp: piecewise-linear lookup in a table, clamping outside it.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	xp := []float64{0, 10, 20} // table positions
	fp := []float64{0, 100, 0} // table values

	dst := make([]float64, 3)
	simd.InterpInto(dst, []float64{5, 15, 99}, xp, fp)
	fmt.Println(dst)
}
Output:
[50 50 0]

func IntersectInto

func IntersectInto[T Integer](dst, a, b []T) int

IntersectInto writes the elements present in both a and b to dst, in ascending order, and returns how many there were.

a and b must be sorted ascending with no duplicates. dst must have room for min(len(a), len(b)) elements, which is the most an intersection can produce; it panics otherwise, because the kernel is at the six-argument limit its ABI allows and cannot be told the destination's length.

Example

IntersectInto keeps the elements present in both. Both inputs must be sorted and free of duplicates — the shape a posting list is already in.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []int32{1, 3, 5, 7, 9}
	b := []int32{3, 4, 5, 6, 9}

	dst := make([]int32, min(len(a), len(b)))
	n := simd.IntersectInto(dst, a, b)
	fmt.Println(dst[:n])
}
Output:
[3 5 9]

func IsASCII

func IsASCII[S Text](s S) bool

IsASCII reports whether every byte is below 0x80.

It is worth checking before text processing, because the ASCII path of most algorithms is dramatically simpler than the general one.

func IsFiniteInto

func IsFiniteInto[T Float](dst []bool, a []T, scratch []T)

IsFiniteInto writes to dst whether each element of a is neither infinite nor NaN.

A NaN fails the comparison against +Inf as unordered rather than as less than, so this cannot be spelled as "not infinite" — the magnitude has to be strictly less than infinity, which excludes both.

func IsInfInto

func IsInfInto[T Float](dst []bool, a []T, scratch []T)

IsInfInto writes to dst whether each element of a is an infinity of either sign.

The magnitude of an infinity is +Inf and the magnitude of everything else is finite or NaN, so one absolute value and one comparison against +Inf answers it. scratch is working space of at least len(a); if it is shorter this allocates one, so pass it on a hot path and omit it otherwise.

func IsNaNInto

func IsNaNInto[T Float](dst []bool, a []T)

IsNaNInto writes to dst whether each element of a is a NaN.

It is NotEqualInto of a against itself, which is exactly what the IEEE definition of NaN says: the only value not equal to itself. Every NaN payload, quiet and signalling alike, answers true.

dst and a must be the same length; the shorter bounds the work.

Example
package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	mask := make([]bool, 3)
	simd.IsNaNInto(mask, []float64{1, math.NaN(), 3})
	fmt.Println(mask)
}
Output:
[false true false]

func L1Norm

func L1Norm[T Number](a []T) T

L1Norm returns the sum of the absolute values of all elements, also called the taxicab or Manhattan norm.

func Lanes

func Lanes[T float32 | float64 | int32 | int64 | uint8]() int

Lanes reports how many elements of type T the widest usable vector holds, and returns 0 here because this build has no vector type. See the amd64 version for what it reports when there is one.

Check for zero rather than dividing by it.

func LastIndex

func LastIndex[S, T Text](haystack S, needle T) int

LastIndex returns the index of the last occurrence of needle in haystack, or -1. An empty needle returns len(haystack).

It matches bytes.LastIndex and strings.LastIndex.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.LastIndex("a,b,c", ","))
}
Output:
3

func LastIndexByte

func LastIndexByte[S Text](s S, c byte) int

LastIndexByte returns the index of the last occurrence of c in s, or -1.

func LastIndexNotAny

func LastIndexNotAny[S, T Text](s S, chars T) int

LastIndexNotAny returns the index of the last byte of s that is not in chars, or -1 if every byte is.

func LayerNorm

func LayerNorm[T Float](a []T, eps T)

LayerNorm rescales a to zero mean and unit variance, with eps added to the variance before the square root to keep the division stable when the input is nearly constant.

This is Standardize with the epsilon that neural network layers require.

Example

LayerNorm centres and scales a vector to zero mean and unit variance — the normalization a transformer block does between every sublayer. One pass, where Mean followed by StdDev followed by the arithmetic is four.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3, 4}
	simd.LayerNorm(a, 1e-5)
	fmt.Printf("%.3f %.3f %.3f %.3f\n", a[0], a[1], a[2], a[3])
}
Output:
-1.342 -0.447 0.447 1.342

func LayerNormInto

func LayerNormInto[T Float](dst, a, gamma, beta []T, eps T)

LayerNormInto is LayerNorm with the learned per-element scale and offset a transformer applies after normalizing:

dst[i] = (a[i] - mean) / sqrt(variance + eps) * gamma[i] + beta[i]

LayerNorm normalizes and stops, which is only half of the layer as it is used in practice: gamma and beta are parameters the model learns, and without them the layer cannot represent the identity.

The mean and the variance are the same two-pass ones Variance computes, so this agrees with normalizing and then applying the affine transform by hand. It writes min of every argument's length and allocates nothing.

Example

The Into form applies the learned gamma and beta in the same pass.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3, 4}
	gamma := []float64{2, 2, 2, 2}
	beta := []float64{1, 1, 1, 1}

	dst := make([]float64, 4)
	simd.LayerNormInto(dst, a, gamma, beta, 1e-5)
	fmt.Printf("%.3f %.3f\n", dst[0], dst[3])
}
Output:
-1.683 3.683

func LeadingZerosInto

func LeadingZerosInto[T Integer](dst, a []T)

LeadingZerosInto writes the number of leading zero bits of each element of a to dst. Zero gives the element width, as in math/bits.LeadingZeros.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]uint8, 1)
	simd.LeadingZerosInto(dst, []uint8{0b0001_0000})
	fmt.Println(dst[0])
}
Output:
3

func LeakyReLU

func LeakyReLU[T Float](a []T, slope T)

LeakyReLU scales negative elements by slope instead of zeroing them, which keeps a gradient flowing where ReLU would stop it.

func Lerp

func Lerp[T Number](a, b []T, t T)

Lerp interpolates each element of a towards b by t: a[i] += (b[i]-a[i]) * t.

t is not clamped, so values outside [0, 1] extrapolate. The evaluation form is monotonic in t and lands exactly on b at t=1, which the algebraically equal a*(1-t) + b*t does not.

func LerpInto

func LerpInto[T Number](dst, a, b []T, t T)

LerpInto sets dst[i] = a[i] + (b[i]-a[i])*t. dst may alias a or b.

func LessEqualInto

func LessEqualInto[T Number](dst []bool, a, b []T)

LessEqualInto writes whether a[i] <= b[i].

func LessEqualScalarInto

func LessEqualScalarInto[T Number](dst []bool, a []T, v T)

LessEqualScalarInto writes whether a[i] <= v.

func LessInto

func LessInto[T Number](dst []bool, a, b []T)

LessInto writes whether a[i] < b[i].

func LessScalarInto

func LessScalarInto[T Number](dst []bool, a []T, v T)

LessScalarInto writes whether a[i] < v.

func LinearRegression

func LinearRegression[T Float](x, y []T) (slope, intercept T)

LinearRegression fits y = slope*x + intercept by ordinary least squares.

It returns zeros if there are fewer than two points or if every x is the same, since no line is determined in either case.

func Log

func Log[T Float](a []T)

Log replaces every element with its natural logarithm.

Zero yields -Inf and a negative input yields NaN, following IEEE 754.

Example
package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, math.E}
	simd.Log(a)
	fmt.Printf("%.4f %.4f\n", a[0], a[1])
}
Output:
0.0000 1.0000

func Log1p

func Log1p[T Float](a []T)

Log1p replaces every element x with log(1+x).

Use it instead of adding one and calling Log when x is near zero, where that form loses almost all its significant digits.

func Log1pInto

func Log1pInto[T Float](dst, a []T)

Log1pInto sets dst[i] = log(1+a[i]), accurately near zero. dst may alias a.

func Log2

func Log2[T Float](a []T)

Log2 replaces every element with its base-2 logarithm.

func Log2Into

func Log2Into[T Float](dst, a []T)

Log2Into sets dst[i] to the base-2 logarithm of a[i]. dst may alias a.

func Log10

func Log10[T Float](a []T)

Log10 replaces every element with its base-10 logarithm.

func Log10Into

func Log10Into[T Float](dst, a []T)

Log10Into sets dst[i] to the base-10 logarithm of a[i]. dst may alias a.

func LogInto

func LogInto[T Float](dst, a []T)

LogInto sets dst[i] to the natural logarithm of a[i]. dst may alias a.

func LogSumExp

func LogSumExp[T Float](a []T) T

LogSumExp returns log(sum(exp(a))) without overflowing, by factoring out the maximum. It is the normalizing constant of Softmax, and appears wherever log-probabilities are combined.

func LowerBoundInto

func LowerBoundInto[T Number](dst []int32, a, q []T)

LowerBoundInto fills dst with, for each element of q, the number of elements of a strictly less than it — the index std::lower_bound and sort.SearchInts return, and the position at which the query would be inserted to keep a sorted.

a must be sorted ascending. dst and q must be the same length; the shorter bounds the work. Nothing is allocated.

Why the batch form and not a single search

One binary search is log2(n) probes and every probe's address comes from the previous comparison, which is a dependency no vector unit helps with — a branchless scalar search is already close to optimal for one query.

Many queries are a different problem. They all walk the same number of steps over the same table, so the loop nest turns inside out: step on the outside, query on the inside. The inner loop is then elementwise over the batch, and the only thing it needs beyond arithmetic is a gather, because each lane probes a different element.

That gather is also the limit. Where the instruction exists — AVX2, AVX-512, SVE2, RVV — this is genuinely vectorized; where it does not, LLVM declines and the portable bisection runs instead. Same wall as the scatter family, recorded in docs/wrong.md entry 59.

Duplicates in a are fine and behave as lower_bound does: the index of the first equal element. For the index after the last, subtract from the count of elements less than or equal, which is LowerBound of the next representable value.

Example

LowerBoundInto is a binary search done for many queries at once. One search is a chain of dependent probes and vectorizes on nothing; a batch turns the loop nest inside out and becomes elementwise over the queries.

Each answer is the number of elements strictly less than the query, which is the index std::lower_bound and sort.SearchInts return — so a query equal to a table entry lands *on* it, not after it. 10 below is at index 1, not 2.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	table := []float64{0, 10, 20, 30}
	queries := []float64{5, 25, 10, 99}

	pos := make([]int32, len(queries))
	simd.LowerBoundInto(pos, table, queries)
	fmt.Println(pos)
}
Output:
[1 3 1 4]

func ManhattanDistance

func ManhattanDistance[T Number](a, b []T) T

ManhattanDistance returns the sum of the absolute differences between a and b, also called the L1 or taxicab distance.

func MaskBits added in v1.4.0

func MaskBits[S Text](dst []byte, s S, c byte)

MaskBits writes one bit per byte of s into dst, set where the byte equals c.

Bit i of dst[i/8] describes s[i], least-significant bit first, so dst is an eighth the size of s however many bytes match. dst must have room for MaskLen(len(s)) bytes; it panics otherwise, because a short mask is a silently wrong answer rather than a truncated one.

This is IndexAll in a different representation, and which one to want depends on what happens next. A list of offsets is smaller when matches are rare and is what a loop over the matches needs. A bitmask is smaller when they are common, and is the right form when the next question is itself bitwise — "which of these are inside a quoted string" is a shift and an and-not over a mask, and a search per offset over a list.

Example

MaskBits answers the same question as IndexAll in the form a bitwise parser wants: a bit per byte rather than a list of offsets.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	doc := []byte(`{"a":1}`)

	mask := make([]byte, simd.MaskLen(len(doc)))
	simd.MaskBits(mask, doc, '"')

	// Bit i of mask[i/8] describes doc[i], least-significant bit first.
	// Quotes at 1 and 3, so bits 1 and 3, printed most-significant first.
	fmt.Printf("%08b\n", mask[0])
}
Output:
00001010

func MaskBitsAny added in v1.4.0

func MaskBitsAny[S Text](dst []byte, s S, chars string)

MaskBitsAny is MaskBits for a set: the bit is set where the byte equals any of the bytes in chars.

At most eight distinct bytes, for the reason given on IndexAllAny. A longer set falls back to the portable path.

Example

MaskBitsAny is MaskBits for a set of up to eight bytes.

The mask is an eighth the size of the input however many bytes match, which is what makes it the cheaper form on input where matches are common — and it is the form to want when the next question is itself bitwise.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	doc := []byte(`{"a":[1,2]}`)

	mask := make([]byte, simd.MaskLen(len(doc)))
	simd.MaskBitsAny(mask, doc, "{}[]:,")

	var at []int
	for i := range doc {
		if mask[i/8]&(1<<(i%8)) != 0 {
			at = append(at, i)
		}
	}
	fmt.Println(at)
}
Output:
[0 4 5 7 9 10]

func MaskBitsLess added in v1.4.0

func MaskBitsLess[S Text](dst []byte, s S, c byte)

MaskBitsLess is MaskBits for an inequality: the bit is set where the byte is below c.

It answers the range questions a set of eight cannot. "Is there a control character here" is thirty-two values as a set and one call as a comparison.

Example

MaskBitsLess covers the range tests a set of eight cannot express.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// A JSON string may not contain a raw control character.
	s := []byte("ok\tno")

	mask := make([]byte, simd.MaskLen(len(s)))
	simd.MaskBitsLess(mask, s, 0x20)

	fmt.Println(mask[0] != 0)
}
Output:
true

func MaskLen added in v1.4.0

func MaskLen(n int) int

MaskLen is how many bytes a MaskBits destination needs for n input bytes.

func MatMulInto

func MatMulInto[T Number](dst, a, b []T, m, k, n int)

MatMulInto multiplies an m*k matrix by a k*n matrix into an m*n one, all in row-major order. dst is zeroed first.

It does nothing if the slices are too short for the stated dimensions, so check your sizes.

// 2x3 times 3x2 into 2x2
simd.MatMulInto(dst, a, b, 2, 3, 2)

Each output element is one accumulator summed over the shared dimension in ascending order, which is what makes every instruction set here agree bit for bit. Zeros in a are not treated specially: a zero times an infinity is a NaN, as IEEE 754 says and as BLAS and numpy both produce.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{ // 2x3
		1, 2, 3,
		4, 5, 6,
	}
	b := []float64{ // 3x2
		7, 8,
		9, 10,
		11, 12,
	}

	dst := make([]float64, 2*2)
	simd.MatMulInto(dst, a, b, 2, 3, 2)

	fmt.Println(dst[:2])
	fmt.Println(dst[2:])

}
Output:
[58 64]
[139 154]

func MatMulIntoPacked

func MatMulIntoPacked[T Float](dst, a, bp []T, m, k, n int)

MatMulIntoPacked multiplies the m-by-k matrix a against a B previously packed by PackBInto, writing the m-by-n result to dst. dst is zeroed first. It does nothing if any slice is too short for the stated shape.

Bit-identical to MatMulInto of the same operands, by construction.

func MatMulIntoScratch

func MatMulIntoScratch[T Float](dst, a, b, scratch []T, m, k, n int)

MatMulIntoScratch is MatMulInto with caller-supplied working space, switching to the packed algorithm when B is large enough that packing wins.

scratch needs GemmPackLen(k, n) elements for the packed path; a shorter one — including nil — falls back to the plain kernel, costing speed and never correctness. Results are bit-identical on either path.

func MatMulParallelInto added in v1.1.0

func MatMulParallelInto[T Number](dst, a, b []T, m, k, n int)

MatMulParallelInto is MatMulInto across several goroutines.

dst is m×n, a is m×k and b is k×n, all row-major, and the result is bit-identical to MatMulInto on the same input — the work is divided by output row, so no element's accumulation order changes.

It uses up to GOMAXPROCS goroutines and returns once they are done. Below roughly a million multiply-accumulates, or when GOMAXPROCS is 1, it runs the serial kernel instead.

Use this when the multiply is the whole job. If you are already running work in parallel — one goroutine per matrix in a batch, say — use MatMulInto and keep the parallelism where you can see it.

Example

MatMulParallelInto is MatMulInto spread across goroutines. It is opt-in because a library that fans out on its own steals cores from a caller that may already be using them — reach for it when the multiply is the whole job, and stay with MatMulInto when you are running a batch of them in parallel yourself.

The result is bit-identical to MatMulInto: the work divides by output row, so no element's accumulation order changes. Below a few million multiply-accumulates it runs the serial kernel anyway, which is why this small example prints the same thing either way.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	//  [1 2]   [5 6]   [19 22]
	//  [3 4] * [7 8] = [43 50]
	a := []float64{1, 2, 3, 4} // 2x2, row-major
	b := []float64{5, 6, 7, 8} // 2x2

	dst := make([]float64, 4)
	simd.MatMulParallelInto(dst, a, b, 2, 2, 2)

	fmt.Println(dst)
}
Output:
[19 22 43 50]

func Max

func Max[T Number](a []T) T

Max returns the largest element.

For floats this is the IEEE 754-2019 maximum: NaN propagates, and +0 is larger than -0. It panics on an empty slice.

For the elementwise maximum of two slices, see Maximum.

func Maximum

func Maximum[T Number](a, b []T)

Maximum keeps the larger of each pair: a[i] = max(a[i], b[i]).

This is the elementwise operation. For the largest element of one slice, see Max.

func MaximumInto

func MaximumInto[T Number](dst, a, b []T)

MaximumInto sets dst[i] = max(a[i], b[i]). dst may alias a or b.

func Mean

func Mean[T Float](a []T) T

Mean returns the arithmetic mean, or zero for an empty slice.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Mean([]float64{1, 2, 3, 4}))
}
Output:
2.5

func Median

func Median[T Number](a []T) T

Median returns the median of a, **reordering a in the process**.

The reordering is what keeps this allocation-free; copy the slice first if you need to keep its order. For an even number of elements the two middle values are averaged, so the result need not be an element of the input. NaN sorts to the end and so does not corrupt the middle.

Where the two middle values compare equal but differ in bits — the only case being negative and positive zero — which one is returned may differ between the accelerated and portable paths. See the note on Sort.

Above a threshold it runs a quickselect around the accelerated partition and allocates a scratch slice to do it, for the same reason Sort does: the partition kernel is out of place and needs somewhere to write. Use MedianInto on a hot path to supply that scratch yourself and allocate nothing. Below the threshold, and on architectures with no compress instruction, it stays on the scalar quickselect and allocates nothing.

It panics on an empty slice.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Median([]float64{3, 1, 4, 1, 5}))
}
Output:
3

func MedianInto

func MedianInto[T Number](a, scratch []T) T

MedianInto is Median using scratch as working space, allocating nothing.

scratch must be at least as long as a; if it is shorter, this falls back to the scalar quickselect rather than panicking, so a short scratch costs speed and not correctness. Its contents afterwards are unspecified, and a is reordered exactly as Median reorders it.

scratch := make([]T, len(a))   // once
for _, batch := range batches {
    m := simd.MedianInto(batch, scratch)
}
Example

MedianInto sorts into the scratch slice instead of allocating one.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{3, 1, 4, 1, 5}
	fmt.Println(simd.MedianInto(a, make([]float64, len(a))))
}
Output:
3

func Min

func Min[T Number](a []T) T

Min returns the smallest element.

For floats this is the IEEE 754-2019 minimum: NaN propagates, and -0 is smaller than +0. It panics on an empty slice.

For the elementwise minimum of two slices, see Minimum.

func MinMax

func MinMax[T Number](a []T) (lo, hi T)

MinMax returns the smallest and largest elements in a single pass, which is cheaper than calling Min and Max separately because the data is read once. It panics on an empty slice.

Example

MinMax walks the slice once for both, which matters when the slice does not fit in cache.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	lo, hi := simd.MinMax([]float64{3, 1, 4, 1, 5})
	fmt.Println(lo, hi)
}
Output:
1 5

func Minimum

func Minimum[T Number](a, b []T)

Minimum keeps the smaller of each pair: a[i] = min(a[i], b[i]).

This is the elementwise operation. For the smallest element of one slice, see Min.

func MinimumInto

func MinimumInto[T Number](dst, a, b []T)

MinimumInto sets dst[i] = min(a[i], b[i]). dst may alias a or b.

func MovingAverageInto

func MovingAverageInto[T Number](dst, a []T, width int)

MovingAverageInto writes the mean of each window of the given width, producing len(a)-width+1 elements.

Each window is computed independently rather than by sliding a running total. That is more arithmetic, but a running total accumulates rounding error without bound over a long series and cannot be vectorized, since every output would depend on the one before it.

func Mul

func Mul[T Number](a, b []T)

Mul multiplies a by b, elementwise: a[i] *= b[i].

Integer multiplication wraps on overflow, matching the hardware.

func MulAll

func MulAll[T Number](dst []T, srcs ...[]T)

MulAll is AddAll with multiplication: dst[i] is the product of the corresponding element of every source, left to right, in one pass.

With no sources dst is filled with ones, which is the identity for the operation being folded.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 3)
	simd.MulAll(dst, []float64{1, 2, 3}, []float64{2, 2, 2}, []float64{5, 5, 5})
	fmt.Println(dst)
}
Output:
[10 20 30]

func MulComplex

func MulComplex[C Complex](a, b []C)

MulComplex multiplies a by b elementwise: a[i] *= b[i].

func MulComplexInto

func MulComplexInto[C Complex](dst, a, b []C)

MulComplexInto sets dst[i] = a[i] * b[i]. dst may alias a or b.

func MulInto

func MulInto[T Number](dst, a, b []T)

MulInto sets dst[i] = a[i] * b[i]. dst may alias a or b.

func NanMean

func NanMean[T Float](a []T, scratch []T, mask []bool) (T, int)

NanMean returns the mean of the non-NaN elements of a, and how many there were.

The count is returned rather than discarded because it is the thing a caller needs to know whether the mean means anything: a mean over three surviving points out of a thousand is not a mean. A slice with no non-NaN elements returns NaN and zero rather than dividing by zero.

Example

NanMean returns the mean and how many values it was taken over.

package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, math.NaN(), 3}
	mean, n := simd.NanMean(a, make([]float64, len(a)), make([]bool, len(a)))
	fmt.Println(mean, "over", n)
}
Output:
2 over 2

func NanSum

func NanSum[T Float](a []T, scratch []T, mask []bool) T

NanSum returns the sum of the non-NaN elements of a, treating NaN as absent rather than as poison.

Sum propagates: one NaN anywhere makes the whole answer NaN, which is what IEEE says and usually what you want. This is the other convention — numpy's nansum, R's sum(na.rm=TRUE) — for data with gaps in it.

It is a select and a sum, both accelerated: the NaN lanes are replaced by zero and the ordinary reduction runs over the result. Adding zero is exact, so the answer is the sum of the surviving elements and nothing has been perturbed by the substitution.

scratch and mask are working space of at least len(a); short ones are replaced by allocations. An empty slice, or one that is entirely NaN, sums to zero — the identity, consistently with Sum of an empty slice.

Example
package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, math.NaN(), 3}
	fmt.Println(simd.NanSum(a, make([]float64, len(a)), make([]bool, len(a))))
}
Output:
4

func NeedsEscapeJSON

func NeedsEscapeJSON[S Text](s S) bool

NeedsEscapeJSON reports whether s contains any byte that AppendEscapeJSON would change. The common answer on real data is no, and knowing it costs one accelerated scan — a serializer can then write the input directly.

func Neg

func Neg[T Number](a []T)

Neg negates every element: a[i] = -a[i].

For floats this flips the sign bit, which unlike 0-x is correct for ±0 and NaN. For integers it wraps.

func NegComplex

func NegComplex[C Complex](a []C)

NegComplex negates a in place.

func NegComplexInto

func NegComplexInto[C Complex](dst, a []C)

NegComplexInto sets dst[i] = -a[i]. dst may alias a.

func NegInto

func NegInto[T Number](dst, a []T)

NegInto sets dst[i] = -a[i]. See Neg for semantics.

func Norm

func Norm[T Float](a []T) T

Norm returns the Euclidean length of a: the square root of the sum of squares. Also called the L2 norm.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Norm([]float64{3, 4}))
}
Output:
5

func Normalize

func Normalize[T Float](a []T)

Normalize scales a to unit Euclidean length. A vector of all zeros is left unchanged, since it has no direction to preserve.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{3, 4}
	simd.Normalize(a)
	fmt.Println(a)
}
Output:
[0.6 0.8]

func NotEqualInto

func NotEqualInto[T Number](dst []bool, a, b []T)

NotEqualInto writes whether a[i] != b[i]. This is true when either side is NaN, so it is not the negation of EqualInto.

func NotEqualScalarInto

func NotEqualScalarInto[T Number](dst []bool, a []T, v T)

NotEqualScalarInto writes whether a[i] != v.

func NotMask

func NotMask(a []bool)

NotMask inverts every element, in place.

func NotMaskInto

func NotMaskInto(dst, a []bool)

NotMaskInto sets dst[i] = !a[i]. dst may alias a.

func OnesCountInto

func OnesCountInto[T Integer](dst, a []T)

OnesCountInto writes the number of one bits in each element of a to dst.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]uint8, 1)
	simd.OnesCountInto(dst, []uint8{0b1011})
	fmt.Println(dst[0])
}
Output:
3

func Or

func Or(a, b []byte)

Or sets in a every bit set in b: a[i] |= b[i].

It processes min(len(a), len(b)) bytes and allocates nothing. Use OrInto to write the result elsewhere.

func OrInto

func OrInto(dst, a, b []byte)

OrInto sets dst[i] = a[i] | b[i].

It processes min(len(dst), len(a), len(b)) bytes. dst may alias a or b.

func OrMask

func OrMask(a, b []bool)

OrMask sets a[i] = a[i] || b[i], in place.

func OrMaskInto

func OrMaskInto(dst, a, b []bool)

OrMaskInto sets dst[i] = a[i] || b[i]. dst may alias a or b.

func PackBInto

func PackBInto[T Float](bp, b []T, k, n int)

PackBInto packs the k-by-n row-major matrix b into bp for MatMulIntoPacked. bp must be at least GemmPackLen(k, n) long; it does nothing otherwise.

Packing costs one pass over B. A caller multiplying several A matrices against one B — the classifier-weights shape — packs once:

bp := make([]float64, simd.GemmPackLen[float64](k, n))
simd.PackBInto(bp, weights, k, n)
for _, batch := range batches {
    simd.MatMulIntoPacked(out, batch, bp, m, k, n)
}

func ParseFloats

func ParseFloats[S Text](dst []float64, src S, idx []int32) (int, bool)

ParseFloats converts the fields of src into float64, writing them to dst, and returns how many it converted and whether every one was valid.

It takes the same index slice as ParseInts:

n := simd.IndexAll(idx, line, ',')
idx[n] = int32(len(line))
count, ok := simd.ParseFloats(vals, line, idx[:n+1])

Results are identical to strconv.ParseFloat's, bit for bit, including the sign of zero. Fields the fast path cannot round exactly are passed to strconv rather than approximated, so there is no accuracy tradeoff and no Fast* variant. See the package comment above for what that costs on input that is mostly such fields.

It stops at the first field strconv also rejects and returns that field's index, so a caller can report where the input went wrong.

func ParseInts

func ParseInts[S Text](dst []int64, src S, idx []int32) (int, bool)

ParseInts converts the fields of src into signed integers, writing them to dst, and returns how many it converted and whether every one was valid.

idx holds the offset of each field separator, which is exactly what IndexAll produces, plus a final entry at len(src) if the last field is not separator-terminated:

n := simd.IndexAll(idx, line, ',')
idx[n] = int32(len(line))
count, ok := simd.ParseInts(vals, line, idx[:n+1])

It stops at the first field that is not a valid integer and returns that field's index, so a caller can report where the input went wrong. A field is invalid if it is empty, contains a non-digit after an optional leading + or -, or names a value outside the int64 range — an over-long field is rejected rather than wrapped.

Why the separator scan is not part of this

It is already fast and it is not where the time goes. On 200,000 short CSV fields IndexAll alone runs at 4.06 GB/s, and the same scan followed by strconv.Atoi at 0.83 — so the scan is a fifth of the work and the conversion is the other four fifths. Splitting them lets a caller reuse a scan, and keeps this kernel to the part that was actually slow.

Example

The two-step CSV integer parse: IndexAll finds every separator in one pass, then ParseInts converts the fields between them.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	line := "10,20,30,40"

	idx := make([]int32, len(line)+1)
	n := simd.IndexAll(idx, line, ',')
	// The last field is not separator-terminated, so it needs a sentinel at
	// the end of the input. Without it the final value is silently dropped.
	idx[n] = int32(len(line))

	dst := make([]int64, n+1)
	got, ok := simd.ParseInts(dst, line, idx[:n+1])
	fmt.Println(dst[:got], ok)
}
Output:
[10 20 30 40] true

func ParseUints

func ParseUints[S Text](dst []uint64, src S, idx []int32) (int, bool)

ParseUints is ParseInts over the full uint64 range.

It is a separate kernel rather than a wrapper because the signed one's limit is 2^63: every value above that — half the domain, and the half a caller reaches for uint64 to get — would be rejected by it.

No sign is accepted, not even a leading '+', matching strconv.ParseUint.

func PartitionInto

func PartitionInto[T Number](dst, src []T, pivot T) int

PartitionInto splits src about a pivot: every element strictly less than the pivot is written to the front of dst, everything else after them, and the number that went to the front is returned.

Both sides keep their relative order, so this is a stable partition.

dst must be at least as long as src and must not overlap it. This is the primitive SortInto is built on, exposed because a partition is useful on its own — selecting a quantile, splitting a batch by threshold, bucketing before a scatter — and because it is the part that is accelerated.

n := simd.PartitionInto(dst, src, 0)
negatives, rest := dst[:n], dst[n:]
Example

PartitionInto splits about a pivot and reports where the split landed. Both sides keep their original relative order.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 6)
	n := simd.PartitionInto(dst, []float64{5, 1, 9, 3, 7, 2}, 4)
	fmt.Println(dst[:n], dst[n:])
}
Output:
[1 3 2] [5 9 7]

func PolyEval

func PolyEval[T Number](x, coeffs []T)

PolyEval evaluates a polynomial in place, replacing each x with p(x). Coefficients are lowest order first.

func PolyEvalInto

func PolyEvalInto[T Number](dst, x, coeffs []T)

PolyEvalInto evaluates a polynomial at every point of x, writing the results to dst. Coefficients are lowest order first, so coeffs[0] is the constant term: coeffs = {1, 2, 3} means 1 + 2x + 3x².

It uses Horner's method and reads x once, rather than the pass per coefficient that chaining Mul and AddScalar would cost.

func PopCount

func PopCount[S Text](s S) int

PopCount returns the total number of set bits across every byte of b.

Example

PopCount counts set bits across a whole byte slice, as one number.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.PopCount([]byte{0b1011, 0b1000}))
}
Output:
4

func Pow

func Pow[T Float](a, b []T)

Pow raises each element of a to the corresponding power in b: a[i] **= b[i].

func PowInto

func PowInto[T Float](dst, a, b []T)

PowInto sets dst[i] = a[i] ** b[i]. dst may alias a or b.

func Prod

func Prod[T Number](a []T) T

Prod returns the product of all elements, or one for an empty slice.

Unlike Sum this is evaluated left to right rather than across a fixed accumulator tree. Products overflow and underflow far more readily than sums, so splitting them across lanes changes which intermediate blows up rather than merely changing the rounding — a reassociated product is not the same computation.

Integer multiplication wraps on overflow.

func QMatMulInt8Into

func QMatMulInt8Into(dst []int32, a, b []int8, m, k, n int)

QMatMulInt8Into multiplies two row-major int8 matrices into an int32 destination: an m*k matrix by a k*n matrix, giving m*n.

The accumulator is int32 and that is the point of a separate function. MatMulInto is generic over one element type, so instantiating it at int8 would accumulate in int8 and overflow after two or three terms — two full-scale int8 values already multiply to 16129. Here the worst case is k*127*128, which stays inside int32 up to k = 132097, past any layer anyone runs.

This is what QuantizeInt8 produces tensors for. Follow it with RequantizeInt8Into to get back to int8, or keep the int32 result if a bias or a residual connection is added next.

The result is exact and identical on every instruction set: integer addition is associative, so unlike the float matmul there is no accumulation order to preserve. It does nothing if the slices are too short for the stated dimensions, and it allocates nothing.

Example

The int8 matrix multiply accumulates into int32, because the products of two int8 values overflow int8 immediately. Requantize brings it back down.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// [1 2] * [1 0] = [1 2]
	// [3 4]   [0 1]   [3 4]
	a := []int8{1, 2, 3, 4}
	b := []int8{1, 0, 0, 1}

	acc := make([]int32, 4)
	simd.QMatMulInt8Into(acc, a, b, 2, 2, 2)
	fmt.Println(acc)

	// q = round(acc*scale) + zeroPoint, rounding half to EVEN — so 1*0.5
	// becomes 0 and 3*0.5 becomes 2, which is what the runtimes this
	// interoperates with do and what round-half-away-from-zero would not.
	out := make([]int8, 4)
	simd.RequantizeInt8Into(out, acc, 0.5, 0)
	fmt.Println(out)
}
Output:
[1 2 3 4]
[0 1 2 2]

func Quantile

func Quantile[T Number](a []T, q float64) T

Quantile returns the q-th quantile of a, **reordering a in the process**.

q is clamped to [0, 1]: Quantile(a, 0) is the minimum, 0.5 the median, 1 the maximum. Values between order statistics are interpolated linearly, which is what numpy and R type 7 do, so results are comparable with those tools.

Like Median it reorders rather than copying, which is what keeps it allocation-free. Copy the slice first if you need to keep its order. For integer types the interpolated value truncates toward zero.

Like Median it uses the accelerated partition above a threshold and allocates a scratch slice to do so; QuantileInto takes that scratch from the caller and allocates nothing.

It panics on an empty slice.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Quantile([]float64{1, 2, 3, 4, 5}, 0.5))
}
Output:
3

func QuantileInto

func QuantileInto[T Number](a, scratch []T, q float64) T

QuantileInto is Quantile using scratch as working space, allocating nothing. scratch must be at least as long as a; a shorter one falls back to the scalar quickselect rather than panicking.

func QuantizeInt8

func QuantizeInt8(dst []int8, a []float32, scale float32, zeroPoint int32)

QuantizeInt8 converts float32 to int8 with an affine scale and zero point, which is the quantization every inference runtime uses:

q = clamp(round(x/scale) + zeroPoint, -128, 127)

Rounding is half to even, matching ONNX, PyTorch and TFLite. That is worth stating because the naive form, int8(x/scale + 0.5), rounds half away from zero and disagrees on every exact .5 — which a symmetric scale produces in quantity rather than rarely, so the two differ in the middle of a typical distribution and not only at its edges.

Values outside the representable range saturate rather than wrapping. It writes min(len(dst), len(a)) elements and allocates nothing.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// A symmetric per-tensor scale, the common case for weights.
	w := []float32{-1.0, -0.5, -0.25, 0, 0.25, 0.5, 1.0}
	scale := float32(1.0 / 127)
	q := make([]int8, len(w))
	simd.QuantizeInt8(q, w, scale, 0)
	fmt.Println(q)
}
Output:
[-127 -64 -32 0 32 64 127]

func QuantizePerChannelInt8

func QuantizePerChannelInt8(dst []int8, a []float32, scale []float32, zeroPoint []int32, channels, inner int)

QuantizePerChannelInt8 is QuantizeInt8 with one scale and zero point per output channel rather than one for the whole tensor.

This is what inference actually uses for weights. Output channels are trained independently and their ranges differ by an order of magnitude or more, so a single tensor-wide scale is set by the widest channel and wastes most of the int8 range on every other one — typically one to two bits of effective precision, for no cost beyond storing a scale per channel.

The layout is channels groups of inner consecutive elements: element c*inner+i belongs to channel c. That is how a weight tensor shaped [outChannels][inChannels*kh*kw] already sits in memory, so no rearrangement is needed.

Rounding, saturation and the zero point behave exactly as in QuantizeInt8. It does nothing if the slices are too short for the stated shape, and it allocates nothing.

Example

Quantization to int8 with a scale and zero point: the operation an inference runtime does to every weight and activation.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// Two channels of two values, each with its own scale.
	a := []float32{1, 2, 30, 40}
	scale := []float32{0.5, 10}
	zero := []int32{0, 0}

	dst := make([]int8, len(a))
	simd.QuantizePerChannelInt8(dst, a, scale, zero, 2, 2)
	fmt.Println(dst)
}
Output:
[2 4 3 4]

func QuantizePerChannelUint8

func QuantizePerChannelUint8(dst []uint8, a []float32, scale []float32, zeroPoint []int32, channels, inner int)

QuantizePerChannelUint8 is QuantizePerChannelInt8 into the unsigned range, clamping to [0, 255].

func QuantizeUint8

func QuantizeUint8(dst []uint8, a []float32, scale float32, zeroPoint int32)

QuantizeUint8 is QuantizeInt8 into the unsigned range, clamping to [0, 255]. This is the form TFLite and most mobile runtimes use.

func RFFT

func RFFT(src []float64) []complex128

RFFT returns the non-redundant half of the transform of the real sequence src, allocating the plan, the scratch and the result. len(src) must be even with len(src)/2 a power of two; it returns nil otherwise.

Example

A real-input FFT returns n/2+1 complex bins, because the rest are conjugates and carry no new information.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// A constant signal has all its energy in bin 0.
	spectrum := simd.RFFT([]float64{1, 1, 1, 1})
	fmt.Println(len(spectrum), real(spectrum[0]), real(spectrum[1]))
}
Output:
3 4 0

func RFFTInto

func RFFTInto(p *RFFTPlan, dst []complex128, src []float64, scratch []complex128)

RFFTInto writes the non-redundant half of the Fourier transform of the real sequence src to dst.

src must be at least p.Len() long and dst at least p.OutLen(). scratch must be at least p.Len()/2 long; a shorter one is replaced by an allocation.

dst[k] equals the k-th bin of the full complex transform, for k from 0 to p.Len()/2. The remaining bins are conj(dst[Len()-k]) and are not written.

func RGBToUVInto

func RGBToUVInto(u, v, r, g, b []byte)

RGBToUVInto writes the two full-range (JFIF) chroma planes of three planar colour channels. The luma plane is GrayscaleInto, which computes the same Y — so a full Y'CbCr conversion is the two calls, and a caller who wants luma alone makes one.

They are separate rather than one call because seven arguments is one more than the SysV amd64 ABI passes in registers, and the fused form was declined by the generator on every amd64 tier.

U and V are biased by 128 so they fit a byte, which is what every 8-bit full-range format does. Each chroma row of the matrix sums to zero, so a grey input gives exactly 128 in both planes — which is why a round trip does not tint a greyscale image.

Like GrayscaleInto it is Q8 fixed point, exact, and allocation-free.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	u := make([]byte, 1)
	v := make([]byte, 1)
	simd.RGBToUVInto(u, v, []byte{255}, []byte{0}, []byte{0})
	fmt.Println(u[0], v[0])
}
Output:
85 255

func RK4Step

func RK4Step[T Float](y []T, t, h T, f Derivative[T], w *RK4Workspace[T])

RK4Step advances y by one step of size h using the classical fourth-order Runge-Kutta method, calling f four times.

It is fourth order, so halving the step size cuts the error per step by about sixteen. w must have been made by NewRK4Workspace for a system at least as large as y.

w := simd.NewRK4Workspace[float64](len(y))
for t := 0.0; t < 10; t += h {
    simd.RK4Step(y, t, h, deriv, w)
}

func RMSNorm

func RMSNorm[T Float](a []T, eps T)

RMSNorm divides a by the root mean square of its elements, with eps added for stability.

Unlike LayerNorm it does not subtract the mean, which makes it cheaper and is what several recent transformer architectures use.

func Ramp

func Ramp[T Number](a []T, start, step T)

Ramp fills a with an arithmetic progression: a[i] = start + i*step.

Each element is computed from its own index rather than by accumulating, so rounding error does not build up along the slice and the whole thing vectorizes.

func RandomInto

func RandomInto[T float32 | float64 | uint64](dst []T, seed uint64)

RandomInto fills dst with uniformly distributed values from a counter-based generator seeded by seed.

float32 and float64 get values in [0, 1); uint64 gets the full range. It allocates nothing.

What "counter-based" buys, and why it is not the usual design

Element i depends on the seed and on i, and on nothing else. A conventional generator — xorshift, PCG, Mersenne Twister — threads a state through the loop, so element i+1 cannot begin until element i has finished. That is a serial dependence and it cannot be vectorized at all.

Computing each element from its index instead makes the loop elementwise, and three useful properties fall out of the same change:

	simd.RandomInto(buf, 42)          // the same bytes on every architecture
	simd.RandomInto(buf[1000:], 42)   // the same bytes as the tail of the above

  - **The same stream everywhere.** This is integer arithmetic with no
    accumulation order, so rule 2 applies and there is nothing to negotiate.
    A simulation seeded the same way gives the same answer on your laptop and
    on an ARM server.
  - **No state to carry.** Filling a window gives the same bytes whether or
    not the preceding window was filled, which is what makes a checkpointed
    run resumable without serialising a generator.
  - **Splittable.** Two goroutines filling disjoint halves produce exactly
    what one goroutine filling the whole would.

What it is not

Not cryptographic. The mixing is splitmix64's finalizer, which passes BigCrush and is the standard choice for simulation and initialisation, but an attacker who sees output can recover the seed. Use crypto/rand where that matters.

Not a drop-in for math/rand: the stream is different, and deliberately so — math/rand's is neither reproducible across Go versions nor vectorizable.

Example

A counter-based generator: element i depends on i alone, which is what makes it vectorizable, reproducible across architectures, and splittable across goroutines without any shared state.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := make([]float64, 4)
	simd.RandomInto(a, 42)

	b := make([]float64, 4)
	simd.RandomInto(b, 42)
	fmt.Println(a[0] == b[0], a[0] >= 0 && a[0] < 1)
}
Output:
true true

func Rank

func Rank(v, table []uint64, p int) int

Rank returns the number of set bits in v at positions strictly below p, using a table from RankTableInto.

Exclusive, so Rank(v, t, 0) is 0 and Rank(v, t, len(v)*64) is the total. That is the definition Select inverts: Select(v, t, Rank(v, t, p)) is the first set bit at or after p.

A p past the end returns the total rather than panicking, which is what makes the usual Rank(hi) - Rank(lo) idiom safe at both ends.

Example

Rank counts the set bits below a position. It is exclusive — Rank(v, t, 0) is 0 — which is what makes Select its exact inverse.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	v := []uint64{0b1011}
	table := make([]uint64, len(v)+1)
	simd.RankTableInto(table, v)

	// 0b1011 has bits 0, 1 and 3 set. Below position 2 that is two of them.
	fmt.Println(simd.Rank(v, table, 0), simd.Rank(v, table, 2), simd.Rank(v, table, 64))
}
Output:
0 2 3

func RankOneInto added in v1.2.0

func RankOneInto[T Float](a, x, y []T, alpha T, m, n int)

RankOneInto adds the outer product of two vectors to a matrix:

a[i*n+j] += alpha * x[i] * y[j]

a is m×n row-major, x has m elements and y has n. This is the trailing-matrix update inside an LU decomposition, and BLAS calls it ger.

Rows of a must be exactly n apart. There is no leading-dimension parameter, so a view into a wider matrix has to be copied out first — the six-argument limit on a kernel is the reason, and passing a stride nobody can prove is in-bounds is the alternative that was not taken.

The row scale is computed once per row, so the result is (alpha*x[i])*y[j] rather than alpha*(x[i]*y[j]). Those differ in the last place, and the portable path does it the same way.

Example

RankOneInto adds the outer product of two vectors to a matrix, which BLAS calls ger. It is the trailing-matrix update inside an LU decomposition, and rows of a must be exactly n apart.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{0, 0, 0, 0} // 2x2 of zeros
	x := []float64{1, 2}
	y := []float64{3, 4}

	// a[i][j] += 1 * x[i] * y[j]
	simd.RankOneInto(a, x, y, 1, 2, 2)

	fmt.Println(a)
}
Output:
[3 4 6 8]

func RankTableInto

func RankTableInto(dst, v []uint64)

RankTableInto fills dst with the exclusive prefix population count of v: dst[i] is the number of set bits in v[:i], so dst[0] is 0 and the last entry is the total.

dst must have len(v)+1 entries. It panics otherwise, because a table one short answers queries at the end of the vector wrongly rather than visibly.

Example

Rank and Select are the pair every succinct structure is built on. The table is built once — a population count per word, then a prefix sum — and every query afterwards is O(1) or O(log n).

The table is an *exclusive* prefix and has one more entry than the vector, which is what makes Rank a single addition with no special case at a word boundary and makes Select its exact inverse.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	v := []uint64{0b1011, 0b1100} // 3 bits set in word 0, 2 in word 1

	table := make([]uint64, len(v)+1)
	simd.RankTableInto(table, v)

	fmt.Println("set bits below position 3:", simd.Rank(v, table, 3))
	fmt.Println("position of the 3rd set bit:", simd.Select(v, table, 2))
	fmt.Println("total:", table[len(v)])
}
Output:
set bits below position 3: 2
position of the 3rd set bit: 3
total: 5

func ReLU

func ReLU[T Float](a []T)

ReLU clamps every negative element to zero, the rectified linear unit.

It is expressed as a clamp against a scalar rather than an elementwise maximum against a zero slice, so it needs no second buffer. NaN propagates.

func RealInto

func RealInto[R Float, C Complex](dst []R, a []C)

RealInto writes the real component of each element of a into dst.

func Reciprocal

func Reciprocal[T Float](a []T)

Reciprocal replaces every element with 1/x.

This is a correctly rounded division, not a fast approximation.

func ReciprocalInto

func ReciprocalInto[T Float](dst, a []T)

ReciprocalInto sets dst[i] = 1/a[i].

func ReplaceByte

func ReplaceByte(b []byte, old, new byte)

ReplaceByte replaces every occurrence of old with new, in place.

func ReplaceByteInto

func ReplaceByteInto[S Text](dst []byte, s S, old, new byte)

ReplaceByteInto writes s into dst with every old replaced by new. dst may alias s when s is a []byte.

func RequantizeInt8Into

func RequantizeInt8Into(dst []int8, a []int32, scale float32, zeroPoint int32)

RequantizeInt8Into takes an int32 accumulator back down to int8 with a scale and zero point:

q = clamp(round(acc*scale) + zeroPoint, -128, 127)

Rounding is half to even, matching QuantizeInt8 and the runtimes this interoperates with. Values outside the range saturate rather than wrapping.

It is separate from QMatMulInt8Into rather than fused because a real layer adds a bias to the int32 accumulator first, and because the scale is usually per output channel — see the per-channel note on QuantizeInt8.

It writes min(len(dst), len(a)) elements and allocates nothing.

Example

RequantizeInt8Into brings an int32 accumulator back to int8: q = round(acc*scale) + zeroPoint, rounding half to even and saturating rather than wrapping.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]int8, 4)
	simd.RequantizeInt8Into(dst, []int32{100, 200, 300, 100000}, 0.5, 0)
	fmt.Println(dst)
}
Output:
[50 100 127 127]

func Rescale

func Rescale[T Float](a []T, lo, hi T)

Rescale maps a linearly onto the range [lo, hi], so its smallest element becomes lo and its largest becomes hi. If every element is identical there is no range to map and a is set to lo.

Rescale(a, 0, 1) is the common min-max normalization.

func Reverse

func Reverse[T Number](a []T)

Reverse reverses the order of the elements.

func ReverseBitsInto

func ReverseBitsInto[T Integer](dst, a []T)

ReverseBitsInto writes each element of a with its bits in reverse order to dst.

func ReverseInto

func ReverseInto[T Number](dst, a []T)

ReverseInto writes a into dst in reverse order.

dst may be a itself, but partially overlapping slices are not supported.

func RollingMaxInto

func RollingMaxInto[T Number](dst, a []T, window int)

RollingMaxInto writes the maximum of every window of the given size into dst. See RollingMinInto, whose contract it shares.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{5, 1, 4, 2, 8, 3}

	dst := make([]float64, len(a)-3+1)
	simd.RollingMaxInto(dst, a, 3)
	fmt.Println(dst)
}
Output:
[5 4 8 8]

func RollingMinInto

func RollingMinInto[T Number](dst, a []T, window int)

RollingMinInto writes the minimum of every window of the given size into dst: dst[i] is the smallest element of a[i : i+window].

There are len(a)-window+1 outputs, so dst must have room for that many. A window that is not positive, or is longer than a, writes nothing.

The extreme is IEEE 754-2019 minimum, the same one Min and Minimum use: a window containing a NaN yields NaN, and -0 is smaller than +0.

dst must not overlap a.

Use this below a window of about 48, and a deque above it

The textbook sliding-window minimum is a monotonic deque: two amortized comparisons per element, whatever the window. This does window-1 elementwise passes, which is more arithmetic — but each pass is a plain contiguous minimum, the shape a vector unit is fastest at, and they are tiled so the block being accumulated stays in L1 across all of them. So it does sixteen windows at a time where the deque does one, and the comparison turns on the *window*, not on n. Measured on a Zen 5 at one million float64:

window     this     hand-written deque
     4     0.65 ms   8.35 ms   12.8x
     8     1.35      8.90       6.6x
    16     2.79      8.62       3.1x
    32     5.65      8.44       1.5x
    64    11.2       8.33       0.75x
   256    44.7       8.21       0.18x

The crossover is just above 32, which is four times the eight float64 lanes an AVX-512 register holds. Above roughly 48, write the deque.

This function does not switch to a deque itself, and that is a decision rather than an omission. A deque needs an index ring proportional to the window, which would be the only allocating operation in this library; and getting IEEE minimum out of one is subtle in a way that would not show up in testing — "pop the back while it is worse" does nothing when neither operand orders, so a plain deque holds a NaN without ever reporting it. A third implementation of these semantics is a liability, and the honest thing is to say where this one stops paying. See docs/wrong.md entry 64.

Example

RollingMinInto writes the minimum of every window. There are len(a)-window+1 outputs.

The extreme is IEEE 754-2019 minimum, so a window containing a NaN yields NaN. Below a window of about 48 this beats a hand-written monotonic deque several times over; above it, write the deque — the doc comment has the measurements.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{5, 1, 4, 2, 8, 3}

	dst := make([]float64, len(a)-3+1)
	simd.RollingMinInto(dst, a, 3)
	fmt.Println(dst)
}
Output:
[1 1 2 2]

func Rotate added in v1.2.0

func Rotate[T Float](x, y []T, c, s T)

Rotate applies a Givens rotation to a pair of vectors:

x[i] = c*x[i] + s*y[i]
y[i] = c*y[i] - s*x[i]

Both assignments use the original x[i]. This is BLAS rot, and it is what a QR factorisation and the bidiagonal phase of an SVD are made of.

Both slices are written, over min(len(x), len(y)) elements. They must not overlap.

Example

Rotate applies a Givens rotation to a pair of vectors, using the original x[i] in both assignments. QR factorisation and the bidiagonal phase of an SVD are built out of it.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	x := []float64{1, 0}
	y := []float64{0, 1}

	simd.Rotate(x, y, 0.6, 0.8) // c, s

	fmt.Println(x, y)
}
Output:
[0.6 0.8] [-0.8 0.6]

func Rotl

func Rotl[T Integer](a []T, s uint64)

Rotl rotates a left by s bits in place.

Example

Rotl rotates rather than shifts, so nothing falls off the end.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []uint8{0b10000001}
	simd.Rotl(a, 1)
	fmt.Printf("%08b\n", a[0])
}
Output:
00000011

func RotlInto

func RotlInto[T Integer](dst, a []T, s uint64)

RotlInto writes each element of a rotated left by s bits to dst.

A rotate has no undefined case: the count is reduced modulo the element width, so every count is meaningful and none needs clamping.

func Rotr

func Rotr[T Integer](a []T, s uint64)

Rotr rotates a right by s bits in place.

func RotrInto

func RotrInto[T Integer](dst, a []T, s uint64)

RotrInto writes each element of a rotated right by s bits to dst.

func Round

func Round[T Float](a []T)

Round rounds every element to the nearest integer, with halves going away from zero. This matches math.Round.

func RoundInto

func RoundInto[T Float](dst, a []T)

RoundInto sets dst[i] to a[i] rounded to nearest, halves away from zero.

func RoundToEven

func RoundToEven[T Float](a []T)

RoundToEven rounds every element to the nearest integer, with halves going to the even neighbour. This matches math.RoundToEven and is the default IEEE 754 rounding mode, which makes it the right choice when rounding repeatedly, since it does not accumulate the upward bias that Round does.

func RoundToEvenInto

func RoundToEvenInto[T Float](dst, a []T)

RoundToEvenInto sets dst[i] to a[i] rounded to nearest, halves to even.

func RunLengthDecodeInt32

func RunLengthDecodeInt32(dst, values, lengths []int32) int

RunLengthDecodeInt32 expands runs back into dst, returning how many elements it wrote.

It is a plain Go loop and there is no kernel behind it, which is the honest shape rather than an omission: expansion's output position depends on the running total of the lengths, so it is a serial prefix. docs/tutorial.md makes the same point about ExpandInto — compression's serial half is the store, which an instruction fixes, and expansion's is the load, which none does.

It stops when dst is full and allocates nothing.

func RunLengthEncodeInt32

func RunLengthEncodeInt32(values, lengths, a []int32, scratch []bool) int

RunLengthEncodeInt32 writes the runs of a into values and lengths, returning how many runs there were.

It needs a scratch []bool at least as long as a — passed in rather than allocated, like every other operation here that needs working space. The run-start mask is computed into it by the kernel and then walked once.

scratch := make([]bool, len(col))
vals := make([]int32, len(col))
lens := make([]int32, len(col))
n := simd.RunLengthEncodeInt32(vals, lens, col, scratch)
vals, lens = vals[:n], lens[:n]

values and lengths must each have room for the number of runs, which in the worst case — no two adjacent elements equal — is len(a). It stops early if they are shorter, returning the number of runs written, so a caller who knows the data is run-heavy can size them optimistically and check.

It allocates nothing.

Example

RunLengthEncodeInt32 turns a run of equal values into a value and a count. The run boundaries are found with a vectorized pass; the scratch slice holds them so nothing is allocated.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []int32{7, 7, 7, 9, 9, 4}

	values := make([]int32, len(a))
	lengths := make([]int32, len(a))
	n := simd.RunLengthEncodeInt32(values, lengths, a, make([]bool, len(a)))

	fmt.Println(values[:n], lengths[:n])
}
Output:
[7 9 4] [3 2 1]

func RunStartsBytesInto

func RunStartsBytesInto(dst []bool, a []byte)

RunStartsBytesInto is RunStartsInto for bytes.

func RunStartsInt64Into

func RunStartsInt64Into(dst []bool, a []int64)

RunStartsInt64Into is RunStartsInto for int64.

func RunStartsInto

func RunStartsInto(dst []bool, a []int32)

RunStartsInto marks every element of a that begins a run of equal values: dst[0] is true, and dst[i] is true when a[i] differs from a[i-1].

This is the vector half of RunLengthEncodeInt32 and it is exported on its own because the mask is useful by itself — feed it to CompressInto to keep one representative per run, or count it to find how many distinct runs a column has before deciding whether encoding is worth it.

It writes min(len(dst), len(a)) entries and allocates nothing.

Example

RunStartsInto marks every element that begins a run, which is the vectorizable half of run-length encoding.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]bool, 6)
	simd.RunStartsInto(dst, []int32{7, 7, 7, 9, 9, 4})
	fmt.Println(dst)
}
Output:
[true false false true false true]

func RuneCount

func RuneCount[S Text](s S) int

RuneCount returns the number of runes in s, counting each byte of invalid UTF-8 as one rune.

It matches utf8.RuneCountInString and utf8.RuneCount. The ASCII run is counted without decoding, which is where the time goes in ordinary text.

func SampleStdDev

func SampleStdDev[T Float](a []T) T

SampleStdDev returns the sample standard deviation, the square root of SampleVariance.

func SampleVariance

func SampleVariance[T Float](a []T) T

SampleVariance returns the sample variance, dividing by n-1 rather than n. It returns zero for a slice shorter than two elements.

func SatAdd

func SatAdd[T Saturating](a, b []T)

SatAdd adds b into a with saturation: a[i] = clamp(a[i] + b[i]).

A sum past the element type's maximum gives the maximum, and one past its minimum gives the minimum, instead of wrapping.

It processes min(len(a), len(b)) elements and allocates nothing. Use SatAddInto to write the result elsewhere.

func SatAddInto

func SatAddInto[T Saturating](dst, a, b []T)

SatAddInto sets dst[i] to the saturating sum of a[i] and b[i]. dst may alias a or b.

func SatSub

func SatSub[T Saturating](a, b []T)

SatSub subtracts b from a with saturation: a[i] = clamp(a[i] - b[i]).

For an unsigned type this is the useful one of the pair: the difference clamps at zero rather than wrapping to a huge value.

func SatSubInto

func SatSubInto[T Saturating](dst, a, b []T)

SatSubInto sets dst[i] to the saturating difference of a[i] and b[i]. dst may alias a or b.

func Scale

func Scale[T Number](a []T, s T)

Scale multiplies every element by s: a[i] *= s.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{1, 2, 3}
	simd.Scale(a, 10)
	fmt.Println(a)
}
Output:
[10 20 30]

func ScaleComplex

func ScaleComplex[C Complex, R Float](a []C, s R)

ScaleComplex multiplies a by the real s in place.

func ScaleComplexInto

func ScaleComplexInto[C Complex, R Float](dst, a []C, s R)

ScaleComplexInto sets dst[i] = a[i] * s for a real s. dst may alias a.

Scaling by a real is four multiplies cheaper than a full complex product, which is why it is offered separately rather than left to MulComplex with a slice of reals.

func ScaleInto

func ScaleInto[T Number](dst, a []T, s T)

ScaleInto sets dst[i] = a[i] * s. dst may alias a.

func ScatterInto

func ScatterInto[T Number](dst []T, idx []int32, src []T)

ScatterInto writes src to the given indices of dst: dst[idx[i]] = src[i].

Indices outside dst are skipped. Where two indices collide the later write wins, matching what the hardware instruction does.

This is accelerated on amd64 and riscv64 and portable on arm64, s390x, loong64 and ppc64le, and that split is hardware and not an omission. NEON has no scatter instruction at all — a scatter there is a loop of scalar stores, which is what the portable path already is. SVE2 does have one, but skipping out-of-range indices makes the store predicated, and LLVM declines to form a predicated scatter from this loop; forcing it with vectorize(assume_safety) or an explicit width produced one or two vector registers of incidental use and no scatter, so there is nothing to gain by pushing harder. GatherInto has no such problem, because a gather is a load and every one of these architectures either has the instruction or can synthesise it without predication.

func Select

func Select(v, table []uint64, k int) int

Select returns the position of the k-th set bit of v, counting from zero, or -1 if there are fewer than k+1 set bits.

Binary search over the table for the word, then a walk inside it: O(log n) on the words and at most 64 steps within one. There is no vectorized form because a query reads two words and the search is the whole cost.

Example

Select is Rank's inverse: where is the k-th set bit, counting from zero.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	v := []uint64{0b1011}
	table := make([]uint64, len(v)+1)
	simd.RankTableInto(table, v)

	fmt.Println(simd.Select(v, table, 0), simd.Select(v, table, 2), simd.Select(v, table, 9))
}
Output:
0 3 -1

func SelectInto

func SelectInto[T Number](dst []T, mask []bool, yes, no []T)

SelectInto blends two slices under a mask: dst[i] = mask[i] ? yes[i] : no[i].

This is the branch-free way to apply a condition across a slice, and the operation a vector unit calls a blend. dst may alias yes or no.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	dst := make([]float64, 3)
	simd.SelectInto(dst, []bool{true, false, true},
		[]float64{1, 2, 3}, []float64{10, 20, 30})
	fmt.Println(dst)
}
Output:
[1 20 3]

func Shl

func Shl[T Integer](a []T, s uint64)

Shl shifts a left by s in place.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []uint32{1, 2, 3}
	simd.Shl(a, 4)
	fmt.Println(a)
}
Output:
[16 32 48]

func ShlInto

func ShlInto[T Integer](dst, a []T, s uint64)

ShlInto writes a << s to dst.

A count at or above the element width gives zero, as in Go. The count is unsigned because Go panics on a negative shift and a kernel cannot panic.

func Shr

func Shr[T Integer](a []T, s uint64)

Shr shifts a right by s in place.

func ShrInto

func ShrInto[T Integer](dst, a []T, s uint64)

ShrInto writes a >> s to dst.

The shift is arithmetic for the signed types and logical for the unsigned ones, as in Go. So a count at or above the width gives zero, except for a negative signed value, which gives -1 — sign extension taken to its limit.

func SiLU

func SiLU[T Float](a []T)

SiLU applies x*sigmoid(x), also called swish.

func Sigmoid

func Sigmoid[T Float](a []T)

Sigmoid replaces every element x with the logistic function 1/(1+e**-x).

It is evaluated in whichever of the two algebraically equal forms keeps the exponent negative, so it does not overflow to NaN for large negative inputs the way the naive expression does.

Example

A whole activation layer in one call. Sigmoid guarantees a documented ULP bound; FastSigmoid is a drop-in replacement that trades some of it away.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	x := []float32{-2, -1, 0, 1, 2}

	simd.Sigmoid(x)
	for _, v := range x {
		fmt.Printf("%.4f ", v)
	}
	fmt.Println()

}
Output:
0.1192 0.2689 0.5000 0.7311 0.8808

func SigmoidInto

func SigmoidInto[T Float](dst, a []T)

SigmoidInto sets dst[i] = 1/(1+e**-a[i]). dst may alias a.

func SignInto

func SignInto[T Float](dst []T, a []T, scratch []T, mask []bool)

SignInto writes the sign of each element of a to dst: -1 for a negative, +1 for a positive, and zero for either zero.

NaN propagates. sign(NaN) is NaN rather than zero, which is the rule the rest of this package keeps and the one numpy chose; the alternative — treating a NaN as unsigned and so as zero — quietly turns missing data into a real value. Both zeros give +0, because zero has no sign to report even when its bit pattern has one.

scratch is working space of at least len(a) and mask of at least len(a); short ones are replaced by allocations.

This costs more passes than a kernel would, and still wins

It is four passes over the data — two comparisons and two selects — where a kernel would be one. Measured against the branch-per-element loop it replaces: 1.9us against 2.5us at 4096, 45us against 211us at 65536, and 1.40ms against 4.16ms at a million. The extra traffic costs least where the data is largest, because both versions are then memory bound and only one of them branches.

A single-pass kernel would still be faster and is worth writing if a measurement asks for it. That measurement has now been taken and it does not ask.

func Simpson

func Simpson[T Float](y []T, h T) T

Simpson approximates the integral of a function sampled at n evenly spaced points h apart, using Simpson's rule.

It requires an odd number of samples, that is an even number of intervals, and returns 0 otherwise. Its error falls as h⁴.

func Sin

func Sin[T Float](a []T)

Sin replaces every element with its sine, in radians.

Example
package main

import (
	"fmt"
	"math"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{0, math.Pi / 2}
	simd.Sin(a)
	fmt.Printf("%.4f %.4f\n", a[0], a[1])
}
Output:
0.0000 1.0000

func SinInto

func SinInto[T Float](dst, a []T)

SinInto sets dst[i] to the sine of a[i]. dst may alias a.

func Sinh

func Sinh[T Float](a []T)

Sinh replaces every element with its hyperbolic sine.

func SinhInto

func SinhInto[T Float](dst, a []T)

SinhInto sets dst[i] to the hyperbolic sine of a[i]. dst may alias a.

func Softmax

func Softmax[T Float](a []T)

Softmax converts a to a probability distribution: each element becomes exp(a[i]) divided by the sum of all the exponentials, so the result is non-negative and sums to one.

The maximum is subtracted before exponentiating. That does not change the result mathematically but it is what stops exp overflowing to Inf for large inputs, which is the usual way a naive implementation produces NaN.

func Softplus

func Softplus[T Float](a []T)

Softplus applies log(1+exp(x)), a smooth approximation to ReLU.

It is evaluated as max(x,0) + log1p(exp(-|x|)), which is exact for large inputs of either sign where the direct form overflows or underflows.

func Sort

func Sort[T Number](a []T)

Measured against slices.Sort

float64, Zen 5, this against Go's pdqsort:

n = 1024     random         6.76us   6.81us    even
n = 16384    random          649us    804us    24% faster
n = 262144   random         13.5ms   16.9ms    26% faster
n = 2097152  random          127ms    156ms    24% faster
n = 262144   few-distinct   1.10ms   1.58ms    43% faster
n = 2097152  few-distinct   10.1ms   13.7ms    36% faster
n = 16384    few-distinct   28.7us   22.9us    20% SLOWER

The last row is the one case that still loses, and it is worth saying what changed. It used to lose by 34%, because a median-of-three pivot equal to much of the range sent every copy of itself to the high side and the recursion made no progress against them. extractEqual now takes that run out when the split comes back skewed, which turned the two larger few-distinct sizes from 24% and 17% ahead into 43% and 36%, and cut this row from 34% behind to 20%.

What is left at 16384 is the fixed cost: three passes to detect and remove the equal run, against a range small enough that pdqsort's own duplicate-handling finishes before they pay for themselves. Making that back needs the extraction folded into the partition kernel itself rather than run as separate passes over the result.

Sort sorts a in ascending order, in place.

For floating-point slices NaN sorts to the end, consistently with Median and Quantile and with the IEEE-754-2019 ordering the rest of this package uses; note that this differs from slices.Sort, which orders NaN first.

Negative zero

This is the one place in the package where the accelerated and portable paths may produce different bits for the same input, and it is worth being exact about why. The order is defined by `<`, exactly as slices.Sort and cmp.Less define it, and under `<` negative zero and positive zero compare equal. Which of two equal-comparing values ends up in a given position is then a property of the algorithm, and the two paths run different algorithms — a stable out-of-place partition feeding pdqsort against pdqsort alone. On a 4096-element slice containing both zeros they differed in 848 positions.

Every one of those outputs is a correct ascending sort, and every pair of differing elements is == to the other. Making them agree means giving the zeros a total order, which means a comparator function rather than a bare `<` — measured at 2.5x slower, for a distinction that only math.Signbit can observe. Median and Quantile inherit the same caveat.

Sort allocates. That is unusual for this package and unavoidable here: the accelerated partition is out of place, so it needs somewhere to write. If that matters, use SortInto and supply the scratch yourself.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []float64{3, 1, 4, 1, 5}
	simd.Sort(a)
	fmt.Println(a)
}
Output:
[1 1 3 4 5]

func SortInto

func SortInto[T Number](a, scratch []T)

SortInto sorts a in place using scratch as working space, allocating nothing.

scratch must be at least as long as a. Its contents afterwards are unspecified. This is the form to use in a loop or on a hot path:

scratch := make([]T, len(a))   // once
for _, batch := range batches {
    simd.SortInto(batch, scratch)
}
Example

SortInto is Sort with the workspace handed in, so a loop over many batches allocates once rather than once per batch. It sorts a in place; scratch is workspace and its contents afterwards are unspecified.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	scratch := make([]float64, 3) // allocate once, reuse
	for _, batch := range [][]float64{{3, 1, 4}, {9, 5, 7}} {
		simd.SortInto(batch, scratch)
		fmt.Println(batch)
	}
}
Output:
[1 3 4]
[5 7 9]

func SortedIndex

func SortedIndex[T Number](a []T, v T) (int, bool)

SortedIndex reports where v would be inserted into the sorted slice a to keep it sorted, and whether v is already present. It is a plain binary search, here because it is the natural companion to Sort and callers should not have to reach for a second package to use one with the other.

func SpMVInto

func SpMVInto[T Float](dst []T, values []T, colIdx []int32, rowPtr []int32, x []T)

SpMVInto computes dst = A*x for a matrix A in compressed sparse row form.

values and colIdx hold the nonzeros in row-major order; rowPtr has one entry per row plus a final total, so row r occupies values[rowPtr[r]:rowPtr[r+1]]. dst must have one entry per row — len(rowPtr)-1 of them.

This is the loop SparseDot is meant for, written out so callers do not have to get the row slicing right. It allocates nothing and it is not parallel: the rows are independent, so a caller who wants goroutines should split rowPtr and call this on each piece.

Example

SpMVInto is the row loop written out, for a matrix in compressed sparse row form. It allocates nothing and is not parallel: the rows are independent, so split rowPtr to use goroutines.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	//  [ 1 0 2 ]        [1]        [ 1*1 + 2*3 ]   [7]
	//  [ 0 3 0 ]    *   [2]    =   [ 3*2       ] = [6]
	values := []float64{1, 2, 3}
	colIdx := []int32{0, 2, 1}
	rowPtr := []int32{0, 2, 3}
	x := []float64{1, 2, 3}

	dst := make([]float64, len(rowPtr)-1)
	simd.SpMVInto(dst, values, colIdx, rowPtr, x)
	fmt.Println(dst)
}
Output:
[7 6]

func SparseDot

func SparseDot[T Float](v []T, idx []int32, x []T) T

SparseDot returns the sum of v[i] * x[idx[i]] — one row of a sparse matrix-vector product.

v and idx are the row's values and column indices and must be the same length; the shorter bounds the work. x is the dense vector.

An index outside x contributes nothing rather than panicking, the same contract GatherInto has: these indices are usually computed, and a stray one should not take the process down. Check them yourself if you need strictness.

Example

SparseDot is one row of a sparse matrix-vector product. An index outside x contributes nothing rather than panicking, the same contract GatherInto has.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// Row with nonzeros at columns 0 and 3.
	values := []float64{2, 5}
	colIdx := []int32{0, 3}
	x := []float64{10, 0, 0, 4}

	fmt.Println(simd.SparseDot(values, colIdx, x)) // 2*10 + 5*4
}
Output:
40

func Sqrt

func Sqrt[T Float](a []T)

Sqrt replaces every element with its square root.

A negative input yields NaN, following IEEE 754.

func SqrtInto

func SqrtInto[T Float](dst, a []T)

SqrtInto sets dst[i] to the square root of a[i].

func SquaredDistance

func SquaredDistance[T Number](a, b []T) T

SquaredDistance returns the squared Euclidean distance between a and b.

Prefer it over Distance when comparing distances to each other, since the square root changes nothing about the ordering and costs time.

func Standardize

func Standardize[T Float](a []T)

Standardize rescales a to zero mean and unit standard deviation, the transform usually called a z-score. If every element is identical the standard deviation is zero and a is set to all zeros.

func StdDev

func StdDev[T Float](a []T) T

StdDev returns the population standard deviation, the square root of Variance.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Printf("%.4f\n", simd.StdDev([]float64{2, 4, 4, 4, 5, 5, 7, 9}))
}
Output:
2.0000

func Sub

func Sub[T Number](a, b []T)

Sub subtracts b from a, elementwise: a[i] -= b[i].

func SubComplex

func SubComplex[C Complex](a, b []C)

SubComplex subtracts b from a elementwise: a[i] -= b[i].

func SubComplexInto

func SubComplexInto[C Complex](dst, a, b []C)

SubComplexInto sets dst[i] = a[i] - b[i]. dst may alias a or b.

func SubInto

func SubInto[T Number](dst, a, b []T)

SubInto sets dst[i] = a[i] - b[i]. dst may alias a or b.

func SubScalar

func SubScalar[T Number](a []T, s T)

SubScalar subtracts s from every element: a[i] -= s.

func SubScalarInto

func SubScalarInto[T Number](dst, a []T, s T)

SubScalarInto sets dst[i] = a[i] - s. dst may alias a.

func Sum

func Sum[T Number](a []T) T

Sum returns the sum of all elements, or zero for an empty slice.

Integer summation wraps on overflow.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Sum([]float64{1, 2, 3, 4}))
}
Output:
10

func SumComplex

func SumComplex[C Complex](a []C) C

SumComplex returns the sum of a.

Both components accumulate into the same fixed number of lanes the real Sum uses, so the answer does not change with the machine's vector width.

func SumSquares

func SumSquares[T Number](a []T) T

SumSquares returns the sum of the squares of all elements.

This is Dot(a, a), and is the un-rooted form of Norm.

func Swap added in v1.2.0

func Swap[T Number](x, y []T)

Swap exchanges the contents of two slices, over min(len(x), len(y)) elements.

Trivial and worth having: pivoting a decomposition swaps rows, which makes this one of the most-called routines in gonum's linear algebra. The slices must not overlap.

Example

Swap exchanges two slices. Pivoting a decomposition swaps rows, which makes it one of the most-called routines in gonum's linear algebra.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	x := []int32{1, 2, 3}
	y := []int32{4, 5, 6}

	simd.Swap(x, y)

	fmt.Println(x, y)
}
Output:
[4 5 6] [1 2 3]

func Tan

func Tan[T Float](a []T)

Tan replaces every element with its tangent, in radians.

func TanInto

func TanInto[T Float](dst, a []T)

TanInto sets dst[i] to the tangent of a[i]. dst may alias a.

func Tanh

func Tanh[T Float](a []T)

Tanh replaces every element with its hyperbolic tangent.

func TanhInto

func TanhInto[T Float](dst, a []T)

TanhInto sets dst[i] to the hyperbolic tangent of a[i]. dst may alias a.

func Tier

func Tier() string

Tier returns the name of the instruction-set tier whose kernels this process is actually running, such as "avx2", "neon" or "scalar".

This is the backend in use, which is not always the best tier the CPU supports: if no kernels have been generated for that tier yet, the next one down is used instead. Describe reports both.

func Tile

func Tile[T Number](a, pattern []T)

Tile fills a by repeating pattern, truncating the last copy if it does not fit evenly. A empty pattern leaves a unchanged.

func ToLowerASCII

func ToLowerASCII(b []byte)

ToLowerASCII maps A-Z to a-z in place, leaving every other byte alone. See ToUpperASCII on why this is UTF-8 safe.

func ToLowerASCIIInto

func ToLowerASCIIInto[S Text](dst []byte, s S)

ToLowerASCIIInto writes the ASCII-lowercased s into dst. dst may alias s when s is a []byte.

func ToUpperASCII

func ToUpperASCII(b []byte)

ToUpperASCII maps a-z to A-Z in place, leaving every other byte alone.

Only ASCII is folded, which makes this safe to run over UTF-8: continuation bytes are all 0x80 or above and are untouched. For full Unicode folding use the strings package; that is not a vectorizable operation.

func ToUpperASCIIInto

func ToUpperASCIIInto[S Text](dst []byte, s S)

ToUpperASCIIInto writes the ASCII-uppercased s into dst. dst may alias s when s is a []byte.

func TopK

func TopK[T Number](a []T, k int) []T

TopK is TopKInto allocating both the destination and the scratch.

Example

The k largest without sorting: TopK selects around the k-th order statistic in linear time, so taking the top 3 of a million never sorts the million.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	scores := []float64{12, 99, 7, 45, 68, 99, 3, 81}
	fmt.Println(simd.TopK(scores, 3))
}
Output:
[99 99 81]

func TopKInto

func TopKInto[T Number](dst []T, a []T, k int, scratch []T) int

TopKInto writes the k largest elements of a to dst, in descending order, and returns how many it wrote — min(k, len(a), len(dst)).

It does not sort a, and it does not sort more of a than it has to: the quickselect that Median uses partitions around the k-th largest in linear time, and only the k elements to its right are then sorted. Sorting the whole slice to take the top ten of a million is the thing this exists to avoid.

NaN sorts to the end, as in Sort, so NaNs are the last thing returned and only when k reaches them.

a is reordered. scratch is working space of at least len(a); a shorter one falls back to a sort rather than failing, which costs speed and not correctness.

func TrailingZerosInto

func TrailingZerosInto[T Integer](dst, a []T)

TrailingZerosInto writes the number of trailing zero bits of each element of a to dst. Zero gives the element width, as in math/bits.TrailingZeros.

func Transpose

func Transpose[T Number](a []T, m, n int) []T

Transpose is TransposeInto allocating the destination.

func TransposeInto

func TransposeInto[T Number](dst, a []T, m, n int)

TransposeInto writes the m*n row-major matrix a as an n*m row-major matrix into dst.

It does nothing if either slice is too short for the stated dimensions, so check your sizes — the same contract MatMulInto has.

dst must not overlap a. An in-place transpose of a non-square matrix is a different algorithm entirely, a permutation into cycles, and it is not what this is.

The kernel walks the matrix in square blocks rather than row by row. Written the obvious way the loop reads a contiguously and writes dst with a stride of m, so every write lands in a different cache line and a matrix wider than the cache evicts each line before its next element arrives. Blocking keeps a block's rows and columns resident together, so each line is filled before it leaves.

Example

TransposeInto is blocked rather than a naive double loop, because the naive one strides the whole row length on every element and misses the cache on each.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	// 2x3 -> 3x2
	a := []float64{1, 2, 3, 4, 5, 6}
	dst := make([]float64, 6)
	simd.TransposeInto(dst, a, 2, 3)
	fmt.Println(dst)
}
Output:
[1 4 2 5 3 6]

func Trapezoid

func Trapezoid[T Float](y []T, h T) T

Trapezoid approximates the integral of a function sampled at n evenly spaced points h apart, using the trapezoidal rule.

Its error falls as h². For smooth functions Simpson is usually far better for the same samples.

func TrimAny

func TrimAny[S, T Text](s S, cutset T) S

TrimAny returns s with every leading and trailing byte that is in cutset removed.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Printf("%q\n", simd.TrimAny("xxhelloxx", "x"))
}
Output:
"hello"

func TrimLeftAny

func TrimLeftAny[S, T Text](s S, cutset T) S

TrimLeftAny returns s with every leading byte that is in cutset removed.

As with IndexAny this is a byte-set operation, not a rune-set one, so it matches strings.TrimLeft only for an ASCII cutset.

func TrimRightAny

func TrimRightAny[S, T Text](s S, cutset T) S

TrimRightAny returns s with every trailing byte that is in cutset removed.

func TrimSpaceASCII

func TrimSpaceASCII[S Text](s S) S

TrimSpaceASCII returns s with leading and trailing ASCII whitespace removed.

It is strings.TrimSpace restricted to the six bytes ASCII calls space, which is what a protocol parser wants: the Unicode set adds NEL, NBSP and the whole of the Zs category, none of which appear in a header line and all of which cost a rune decode to recognize.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Printf("%q\n", simd.TrimSpaceASCII("  hello\t\n"))
}
Output:
"hello"

func Trunc

func Trunc[T Float](a []T)

Trunc rounds every element towards zero, discarding the fractional part.

func TruncInto

func TruncInto[T Float](dst, a []T)

TruncInto sets dst[i] to a[i] rounded towards zero. dst may alias a.

func UTF8Len

func UTF8Len(s []uint16) int

UTF8Len returns the number of bytes AppendUTF8 would append for s.

func UTF16Len

func UTF16Len[S Text](s S) int

UTF16Len returns the number of UTF-16 units AppendUTF16 would append for s, so that a caller can size a buffer exactly.

It is a full pass over the input. Sizing with len(s) instead is always safe and never more than a factor of two too large — the worst case is all-ASCII, one unit per byte — so this is for callers who would rather pay the scan than the memory.

func ValidUTF8

func ValidUTF8[S Text](s S) bool

ValidUTF8 reports whether s is entirely well-formed UTF-8.

It matches utf8.Valid and utf8.ValidString.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.ValidUTF8("héllo"), simd.ValidUTF8([]byte{0xff, 0xfe}))
}
Output:
true false

func Variance

func Variance[T Float](a []T) T

Variance returns the population variance: the mean of the squared deviations from the mean. It returns zero for a slice shorter than two elements.

It uses two passes rather than the single-pass sum-of-squares identity, because that identity loses most of its significant digits when the variance is small relative to the mean.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	fmt.Println(simd.Variance([]float64{2, 4, 4, 4, 5, 5, 7, 9}))
}
Output:
4

func VarintLenInto

func VarintLenInto[T VarintValue](dst []int32, a []T)

VarintLenInto writes the LEB128 width of each element of a into dst: 1 to 5 bytes for uint32, 1 to 10 for uint64.

dst and a must be the same length; the shorter bounds the work.

Prefix-summing the result with CumSumInto gives every value its offset in the encoded stream, which is what makes the writes independent.

Example

VarintLenInto gives the per-value widths. Prefix-summed, they are every value's offset in the stream, which is what makes the writes independent.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	lens := make([]int32, 3)
	simd.VarintLenInto(lens, []uint64{1, 300, 70000})
	fmt.Println(lens)
}
Output:
[1 2 3]

func VarintSize

func VarintSize[T VarintValue](a []T) int

VarintSize returns the total number of bytes the whole slice encodes to.

One vectorized pass, no allocation. This is the number to pass to make when building the destination buffer: sized exactly, it is written once, where an append-and-grow encoder copies what it has already written every time the slice doubles.

Example

VarintSize gives the exact encoded length of a whole slice in one vectorized pass, so an encoder sizes its buffer once instead of growing it.

Writing the bytes is serial and always will be — where value i lands depends on the width of every value before it — but asking how wide each one is vectorizes, and that is the part that lets the allocation happen once.

package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []uint64{1, 300, 70000}
	fmt.Println(simd.VarintSize(a), "bytes")
}
Output:
6 bytes

func VerletStep

func VerletStep[T Float](pos, vel, acc []T, h T, accel func(pos, out []T))

VerletStep advances position and velocity by one step of size h using velocity Verlet, given the acceleration at the current and next positions.

It is the standard integrator for molecular dynamics and games because it conserves energy over long runs far better than Euler or even RK4 does, and because it needs only one force evaluation per step.

accel is called with the position to evaluate and the buffer to write the acceleration into. acc holds the acceleration at the current position on entry and at the new position on return, so pass the same slice back on the next step rather than recomputing it.

func Xor

func Xor(a, b []byte)

Xor flips in a every bit set in b: a[i] ^= b[i].

It processes min(len(a), len(b)) bytes and allocates nothing. Use XorInto to write the result elsewhere.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	a := []uint8{0b1100, 0b1010}
	simd.Xor(a, []uint8{0b1010, 0b1010})
	fmt.Println(a)
}
Output:
[6 0]

func XorInto

func XorInto(dst, a, b []byte)

XorInto sets dst[i] = a[i] ^ b[i].

It processes min(len(dst), len(a), len(b)) bytes. dst may alias a or b.

func XorMask

func XorMask(a, b []bool)

XorMask sets a[i] = a[i] != b[i], in place.

func XorMaskInto

func XorMaskInto(dst, a, b []bool)

XorMaskInto sets dst[i] = a[i] != b[i]. dst may alias a or b.

func Zero

func Zero[T Number](a []T)

Zero sets every element to the zero value.

func ZigzagDecodeInt8Into

func ZigzagDecodeInt8Into(dst []int8, a []byte)

ZigzagDecodeInt8Into is the inverse of ZigzagEncodeInt8Into.

func ZigzagDecodeInt16Into

func ZigzagDecodeInt16Into(dst []int16, a []uint16)

ZigzagDecodeInt16Into is the inverse of ZigzagEncodeInt16Into.

func ZigzagDecodeInt32Into

func ZigzagDecodeInt32Into(dst []int32, a []uint32)

ZigzagDecodeInt32Into is the inverse of ZigzagEncodeInt32Into.

func ZigzagDecodeInt64Into

func ZigzagDecodeInt64Into(dst []int64, a []uint64)

ZigzagDecodeInt64Into is the inverse of ZigzagEncodeInt64Into.

func ZigzagEncodeInt8Into

func ZigzagEncodeInt8Into(dst []byte, a []int8)

ZigzagEncodeInt8Into is ZigzagEncodeInt32Into for 8-bit values.

func ZigzagEncodeInt16Into

func ZigzagEncodeInt16Into(dst []uint16, a []int16)

ZigzagEncodeInt16Into is ZigzagEncodeInt32Into for 16-bit values.

func ZigzagEncodeInt32Into

func ZigzagEncodeInt32Into(dst []uint32, a []int32)

ZigzagEncodeInt32Into maps signed integers onto unsigned ones so that a small magnitude of either sign becomes a small unsigned value:

0, -1, 1, -2, 2  ->  0, 1, 2, 3, 4

This is the transform that makes a varint of a negative number short, and it is what protobuf, Avro and delta-encoded column stores apply before varint encoding. Without it, -1 as a two's complement 32-bit value has every high bit set and costs the full five bytes.

The mapping is a bijection: every value round-trips through ZigzagDecodeInt32Into, including math.MinInt32, which is the case a naive negate-and-double formulation overflows on.

It writes min(len(dst), len(a)) elements and allocates nothing.

Example
package main

import (
	"fmt"

	"github.com/sebishogun/simd"
)

func main() {
	deltas := []int32{0, -1, 1, -2, 2}
	enc := make([]uint32, len(deltas))
	simd.ZigzagEncodeInt32Into(enc, deltas)
	// Small magnitudes of either sign became small unsigned values, which is
	// what makes the varint of each one a single byte.
	fmt.Println(enc)
}
Output:
[0 1 2 3 4]

func ZigzagEncodeInt64Into

func ZigzagEncodeInt64Into(dst []uint64, a []int64)

ZigzagEncodeInt64Into is ZigzagEncodeInt32Into for 64-bit values, which is the width protobuf's sint64 uses.

Types

type Accumulator

type Accumulator[T Float] struct {
	// contains filtered or unexported fields
}

Accumulator is a resumable sum over float32 or float64.

The zero value is ready to use. Add it chunks in order; Accumulator.Sum, Accumulator.Mean and Accumulator.Count can be read at any point and do not disturb the accumulation.

var acc simd.Accumulator[float64]
for {
	n, err := read(buf)
	acc.Add(buf[:n])
	if err != nil {
		break
	}
}
fmt.Println(acc.Sum(), acc.Mean())

The result equals Sum over the concatenation, bit for bit, however the chunks fell. It allocates nothing.

func (*Accumulator[T]) Add

func (s *Accumulator[T]) Add(a []T)

Add folds a chunk into the accumulator.

It processes the chunk in three parts, and the split is what preserves the lane assignment: a head of elements up to the next multiple of SumLanes, then whole blocks through the vectorized kernel, then a tail. Only the middle part is accelerated, which is the right trade — the head and tail are at most fifteen elements each per call.

func (*Accumulator[T]) Count

func (s *Accumulator[T]) Count() int

Count returns how many elements have been added.

func (*Accumulator[T]) Mean

func (s *Accumulator[T]) Mean() T

Mean returns the arithmetic mean of everything added so far, or zero if nothing has been.

func (*Accumulator[T]) Reset

func (s *Accumulator[T]) Reset()

Reset returns the accumulator to its zero state, keeping any allocation. There is none to keep, which is the point — it is here so a caller can reuse one in a loop without thinking about it.

func (*Accumulator[T]) Sum

func (s *Accumulator[T]) Sum() T

Sum returns the sum of everything added so far.

It folds a copy of the accumulators, so it can be called at any point without disturbing the accumulation.

type Complex

type Complex interface{ ~complex64 | ~complex128 }

Complex is the constraint for the complex element types.

type Derivative

type Derivative[T Float] func(t T, y, dydt []T)

Derivative computes dy/dt at time t for state y, writing into dydt.

It must not resize or reallocate dydt, and must write every element.

type FFTPlan

type FFTPlan struct {
	// contains filtered or unexported fields
}

FFTPlan holds the tables for transforms of one length. It is safe for concurrent use by multiple goroutines, since transforming does not modify it.

func NewFFTPlan

func NewFFTPlan(n int) *FFTPlan

NewFFTPlan builds a plan for transforms of length n, which must be a power of two and at least 1. It returns nil for any other n.

func (*FFTPlan) Len

func (p *FFTPlan) Len() int

Len returns the transform length the plan was built for.

type Float

type Float interface {
	float32 | float64
}

Float is the element type of operations that only make sense for floats.

type IntAccumulator

type IntAccumulator[T Integer] struct {
	// contains filtered or unexported fields
}

IntAccumulator is a resumable integer sum.

Separate from Accumulator because integer addition is associative, so there is no lane discipline to preserve and the state is one value. It wraps on overflow, like Sum and like the hardware.

func (*IntAccumulator[T]) Add

func (s *IntAccumulator[T]) Add(a []T)

Add folds a chunk in.

func (*IntAccumulator[T]) Count

func (s *IntAccumulator[T]) Count() int

Count returns how many elements have been added.

func (*IntAccumulator[T]) Reset

func (s *IntAccumulator[T]) Reset()

Reset returns the accumulator to its zero state.

func (*IntAccumulator[T]) Sum

func (s *IntAccumulator[T]) Sum() T

Sum returns the sum of everything added so far.

type Integer

type Integer interface {
	int8 | int16 | int32 | int64 |
		uint8 | uint16 | uint32 | uint64
}

Integer is the element type of operations that only make sense for integers: the bitwise ones, and the two saturating ones.

type MinMaxAccumulator

type MinMaxAccumulator[T Float] struct {
	// contains filtered or unexported fields
}

MinMaxAccumulator is a resumable minimum and maximum.

The zero value is ready to use. Unlike Accumulator this needs no lane discipline: minimum and maximum are associative and commutative, so no grouping is observable and the state is just the two values.

NaN propagates the way Min and Max define it: a NaN anywhere in the input makes both results NaN, and it cannot be un-seen by a later chunk.

func (*MinMaxAccumulator[T]) Add

func (s *MinMaxAccumulator[T]) Add(a []T)

Add folds a chunk in.

func (*MinMaxAccumulator[T]) MinMax

func (s *MinMaxAccumulator[T]) MinMax() (min, max T, ok bool)

MinMax returns the minimum and maximum seen so far, and whether anything has been added. Both are zero when nothing has.

func (*MinMaxAccumulator[T]) Reset

func (s *MinMaxAccumulator[T]) Reset()

Reset returns the accumulator to its zero state.

type MultiSearcher

type MultiSearcher struct {
	// contains filtered or unexported fields
}

MultiSearcher searches a haystack for any of a fixed set of needles.

Compile the set once and search many haystacks with it:

m := simd.NewMultiSearcher(keywords)
for _, line := range lines {
    if pos, which := m.Index(line); pos >= 0 {
        ...
    }
}

A MultiSearcher is immutable after construction and safe for concurrent use.

func NewMultiSearcher

func NewMultiSearcher[T Text](needles []T) *MultiSearcher

NewMultiSearcher compiles a needle set. An empty needle matches at position 0, as Index does; duplicate needles are kept, and the earliest-listed one wins a tie.

func (*MultiSearcher) Contains

func (m *MultiSearcher) Contains(haystack []byte) bool

Contains reports whether any needle occurs in haystack.

func (*MultiSearcher) ContainsString

func (m *MultiSearcher) ContainsString(haystack string) bool

ContainsString is MultiSearcher.Contains over a string.

func (*MultiSearcher) Index

func (m *MultiSearcher) Index(haystack []byte) (pos, which int)

Index returns the position of the earliest match of any needle in haystack and which needle matched, or (-1, -1) if none does.

Earliest is by position first and by needle order second, so the result does not depend on which needle the scan happened to find first.

A method cannot take a type parameter in Go, so unlike the free functions in this package these come in []byte and string pairs, as the standard library's own bytes and strings packages do.

func (*MultiSearcher) IndexString

func (m *MultiSearcher) IndexString(haystack string) (pos, which int)

IndexString is MultiSearcher.Index over a string, without copying it.

type Number

type Number interface {
	float32 | float64 |
		int8 | int16 | int32 | int64 |
		uint8 | uint16 | uint32 | uint64
}

Number is the element type of the arithmetic operations.

The types are listed exactly rather than as approximations, so a defined type such as `type Celsius float32` is not accepted. Supporting those would require reinterpreting the slice, which this package does not do.

type RFFTPlan

type RFFTPlan struct {
	// contains filtered or unexported fields
}

RFFTPlan transforms real sequences of a fixed even length.

A real signal's spectrum is conjugate-symmetric, so half of it is redundant and half the work is wasted computing it. This does the standard trick: pack the n real samples into n/2 complex ones by putting the even-indexed samples in the real parts and the odd-indexed in the imaginary parts, run a complex transform of half the length, then untangle. The result is the n/2+1 non-redundant bins, from DC to Nyquist.

Measured against transforming the same real signal as complex, float64, median of five:

n = 1024     5.85us    vs   6.19us      6% faster
n = 65536     532us    vs    818us     54% faster

Short of the factor of two the operation count promises, because the untangling pass is real work and is not free — at 1024 it very nearly eats the whole saving. It also halves the output: n/2+1 bins instead of n, which at 65536 is 512 KiB rather than 1 MiB, and that matters more than the time for anything that keeps spectra around.

func NewRFFTPlan

func NewRFFTPlan(n int) *RFFTPlan

NewRFFTPlan builds a plan for real transforms of length n, which must be even and n/2 must be a power of two — so n is 2, 4, 8, 16 and so on. It returns nil otherwise.

func (*RFFTPlan) Len

func (p *RFFTPlan) Len() int

Len returns the real transform length.

func (*RFFTPlan) OutLen

func (p *RFFTPlan) OutLen() int

OutLen returns the number of bins RFFTInto writes, which is p.Len()/2+1 — DC through Nyquist inclusive. The rest of the spectrum is the conjugate mirror of these and is not written.

type RK4Workspace

type RK4Workspace[T Float] struct {
	// contains filtered or unexported fields
}

RK4Workspace holds the scratch that RK4Step needs, so that stepping a system forward allocates nothing however many steps you take.

Allocate it once with NewRK4Workspace and reuse it for the whole integration.

func NewRK4Workspace

func NewRK4Workspace[T Float](n int) *RK4Workspace[T]

NewRK4Workspace allocates the scratch for integrating a system of n equations. This is the only function in the package that allocates, and it is called once per system rather than once per step.

type Saturating

type Saturating interface {
	int8 | int16 | int32 | uint8 | uint16 | uint32
}

Saturating is the element type of SatAdd and SatSub.

The 64-bit types are absent, and it is a limit of the implementation rather than of the idea. A saturating add is written as a widening add followed by a clamp, which is the form that compiles to the single instruction every vector unit has for it — and there is no integer wider than 64 bits to widen into. Clamping a 64-bit sum needs an overflow test, which does not vectorize into anything worth crossing the call boundary for.

type Text

type Text interface {
	string | []byte
}

Text is the constraint for input a scanning function only reads.

The two types are listed exactly rather than as approximations, the same choice Number makes: a defined type such as `type Token string` is not accepted, because supporting it would mean reinterpreting the value and this package does not do that behind a caller's back.

type VarintValue

type VarintValue interface{ uint32 | uint64 }

VarintValue is the pair of widths LEB128 is defined for here. Signed values are encoded by zigzagging first — see ZigzagEncodeInt32Into — which is what Protocol Buffers calls sint32 and sint64, and what keeps a small negative number one byte rather than ten.

Directories

Path Synopsis
cmd
simdinfo command
Command simdinfo reports which instruction-set tier the simd package selected on this machine, and which ones it could have used.
Command simdinfo reports which instruction-set tier the simd package selected on this machine, and which ones it could have used.
site command
Command site serves a page that runs this library's benchmarks on the machine visiting it, beside the code each one measures.
Command site serves a page that runs this library's benchmarks on the machine visiting it, beside the code each one measures.
docs
examples/csvscan command
Command csvscan is a complete program: it parses a CSV column of numbers and reports statistics on it, doing both halves with this library.
Command csvscan is a complete program: it parses a CSV column of numbers and reports statistics on it, doing both halves with this library.
examples/standardise command
Command standardise is the worked example from docs/tutorial.md, kept here as a program so that `go build ./...` and `go vet ./...` check it on every commit.
Command standardise is the worked example from docs/tutorial.md, kept here as a program so that `go build ./...` and `go vet ./...` check it on every commit.
internal
backend
Package backend is the registry that connects generated assembly to the dispatcher.
Package backend is the registry that connects generated assembly to the dispatcher.
benchmarks
Package benchmarks holds every benchmark in this repository.
Package benchmarks holds every benchmark in this repository.
cpu
Package cpu detects which SIMD instruction-set tier the running CPU supports.
Package cpu detects which SIMD instruction-set tier the running CPU supports.
kernel
Package kernel defines the contract every backend implements.
Package kernel defines the contract every backend implements.
perf
Package perf is a repetition tester in the style Casey Muratori uses in Performance-Aware Programming.
Package perf is a repetition tester in the style Casey Muratori uses in Performance-Aware Programming.
ref
Package ref is the portable Go reference implementation of every kernel.
Package ref is the portable Go reference implementation of every kernel.
tests/arrays
Package arrays holds the arrays tests for github.com/sebishogun/simd.
Package arrays holds the arrays tests for github.com/sebishogun/simd.
tests/docs
Package docs checks the repository's own documentation against the tree: the counts in the README, the identifiers it names, the tiers CONTRIBUTING asks for runs on, and that every operation in the README's index has a runnable example.
Package docs checks the repository's own documentation against the tree: the counts in the README, the identifiers it names, the tiers CONTRIBUTING asks for runs on, and that every operation in the README's index has a runnable example.
tests/dsp
Package dsp holds the dsp tests for github.com/sebishogun/simd.
Package dsp holds the dsp tests for github.com/sebishogun/simd.
tests/encode
Package encode holds the encode tests for github.com/sebishogun/simd.
Package encode holds the encode tests for github.com/sebishogun/simd.
tests/matrix
Package matrix holds the matrix tests for github.com/sebishogun/simd.
Package matrix holds the matrix tests for github.com/sebishogun/simd.
tests/reduce
Package reduce holds the reduce tests for github.com/sebishogun/simd.
Package reduce holds the reduce tests for github.com/sebishogun/simd.
tests/search
Package search holds the search tests for github.com/sebishogun/simd.
Package search holds the search tests for github.com/sebishogun/simd.
tests/text
Package text holds the text tests for github.com/sebishogun/simd.
Package text holds the text tests for github.com/sebishogun/simd.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL