Help us shape the future of Go! Take the 2026 Go Developer Survey

compute

package
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 1 Imported by: 0

Documentation

Overview

Package compute provides vectorized numeric kernels used by gobi's compute layer. Kernels dispatch to a SIMD implementation when built with `GOEXPERIMENT=simd` on a supported architecture (arm64 NEON, amd64 AVX2/AVX-512), and to a portable scalar implementation everywhere else.

Positioning vs arrow-go/arrow/compute

arrow-go ships its own compute package (a "native-go Acero-like engine") at `arrow.compute`. Its Datum-based function-registry API is much more general than gobi's typed-slice kernels — it handles chunked arrays, arbitrary arrow types, promotion rules, null propagation, and overflow modes uniformly. That generality costs per-row overhead.

Measured on arm64 (Apple M3 Pro, arrow-go v18.7.0, Go 1.27rc1) via `benchmarks/arrow_compute/main.go`:

Float64 Add, n=10M rows
  gobi   Series.Add:      755 ps/row  (1324 Mrows/s)
  arrow  compute.Add:    1570 ps/row  ( 637 Mrows/s)   2.1× slower

Float64 → Int64 Cast, n=10M rows
  gobi   Cast:            3838 ps/row  ( 261 Mrows/s)
  arrow  CastArray:        275 ps/row  (3629 Mrows/s)  13.9× faster

Two entirely different stories:

  • For Add (and any arithmetic), the 2× gap holds at 100K, 1M, and 10M rows. That's per-row dispatch cost inside arrow-go's `exec.ArrayKernelExec` + `ExecSpan` traversal, not a flat setup tax that amortizes at large N. On amd64, arrow-go's hand-written AVX2 SIMD kernels close some of that gap on arithmetic, but a 4-lane float64 SIMD win (theoretical 4×) vs a 2× per-row dispatch penalty nets out to a wash — not a transformative win.

  • For Cast, arrow-go's hand-written NEON SIMD kernel (`internal/kernels/cast_numeric_neon_arm64.s`) crushes gobi's scalar builder loop 14× on arm64. The batch nature of casts amortizes the dispatch overhead; the SIMD kernel dominates the runtime.

Overlap surface (measured, not aspirational):

category                  arrow-go/compute       gobi
------------------------  ---------------------  --------------------
Add / Sub / Mul / Div     amd64 SIMD (.s files); keep gobi's — arrow-go's
                          arm64 scalar           per-row dispatch is 2× overhead
Type casts                amd64 + arm64 SIMD     ✅ use arrow-go's — 14× faster
                                                 on arm64 (measured)
Constant-factor scalar    amd64 SIMD             use arrow-go's on amd64
Compare (Eq/Ne/Lt/…)      amd64 SIMD (.s files); keep gobi's — measured 1.4×
                          arm64 scalar           slower on arm64 via arrow-go
Fused compare chains      absent                 unique (BBox, Range,
                                                 WithinSqDist)
Reductions (Sum/Min/Max)  absent                 unique when landed
arm64 SIMD arithmetic     absent                 landed in v0.4.0
                                                 (portable simd, Go 1.27+)

Takeaway

gobi/compute isn't a replacement for arrow-go/compute — different design points. arrow-go/compute is the right choice for one-shot batch operations where the Datum overhead is amortized across the batch AND where the general-arrow-type dispatch actually matters. gobi/compute is the right choice for the tight-inner- loop shapes on the LazyFrame hot path: comparisons, fused compare chains, and (eventually) reductions — the operations that recur per-row in filter/groupby/aggregate pipelines where 2× per-row dispatch overhead is prohibitive.

Survey done in v0.3.9: measured Add / Ge / Cast against arrow-go/compute at n = 100K, 1M, 10M. Cast was the sole clear win (13.9× on arm64 via `cast_numeric_neon_arm64.s`) and got wired into `Expr.Cast`. Everything else in arrow-go's kernel surface either has SIMD only on amd64 (arithmetic, compare, constant-factor mul — Datum dispatch narrows the SIMD win to a wash) or has no SIMD at all (Boolean ops, string compare, rounding, sort, filter, hash, set lookup, temporal casts — scalar Go behind a 2× dispatch tax). arrow-go has NO aggregate/reduction kernels at all — Sum/Min/Max/Mean/etc. remain gobi's territory.

Surface stability

The compute package is INTERNAL to gobi. Its API is not covered by the top-level gobi module's SemVer guarantees — kernels are added, renamed, or specialized without notice. Callers outside gobi should use the higher-level Series / Frame / Expr surface instead.

Build tags

The SIMD path lives behind `//go:build goexperiment.simd && (arm64 || amd64)`. As of Go 1.27 (August 2026) the stdlib `simd` package shipped but remains behind `GOEXPERIMENT=simd` — code linked without the flag gets the scalar path. This file provides the entry points; specific implementations live in *_simd.go / *_scalar.go pairs alongside. When the experiment eventually graduates the tag simplifies to just the arch check.

Adding a kernel

Every kernel is a plain function with typed slice arguments and a `[]bool` or scalar output. No arrow types, no gobi types — that keeps the SIMD-friendly inner loops free of any indirection the compiler can't lower. Callers convert to/from arrow at the package boundary.

Kernel signature convention:

Cmp<Type><Op>(a []<Type>, b <Type>, out []bool)
    element-wise compare against a scalar; writes into out.
Cmp<Type><Op>Vec(a, b []<Type>, out []bool)
    element-wise compare against a same-length column.
Sum<Type>(a []<Type>) <Type>
Min<Type>(a []<Type>) (<Type>, bool)   — bool = "saw a value"
Max<Type>(a []<Type>) (<Type>, bool)

Before adding an arithmetic-shaped kernel here, check whether arrow-go/compute already covers it — most likely it does with better amd64 SIMD than we could match. Fused shapes and comparisons are our unique territory.

Every kernel is safe to call with zero-length inputs and produces the identity value (0 / (0,false) / no-op writes).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AndChainF64BBox

func AndChainF64BBox(a []float64, aLo, aHi float64, b []float64, bLo, bHi float64, out []bool)

AndChainF64BBox writes

out[i] = (aLo <= a[i] <= aHi) && (bLo <= b[i] <= bHi)

in a single pass — the canonical 2D bbox filter shape. Fusing both columns' comparisons into one kernel avoids the intermediate []bool that a "per-column primitive + scalar AND" composition would allocate. Callers that just want a 1D range should use AndChainF64Range instead.

a and b must have equal length; out must have len >= len(a). Panics on length mismatch (invariant guaranteed by callers operating on same-frame columns).

func AndChainF64Range

func AndChainF64Range(a []float64, lo, hi float64, out []bool)

AndChainF64Range writes out[i] = (lo <= a[i]) && (a[i] <= hi). Fused two-sided range check — the shape most bbox filters produce. Branchless; see andChainF64RangeScalar.

func BoundsF64 added in v0.4.1

func BoundsF64(xs, ys []float64) (minX, minY, maxX, maxY float64, ok bool)

BoundsF64 computes the axis-aligned bounding box of the points held in parallel Xs / Ys slices. Returns (minX, minY, maxX, maxY, ok=true) when both slices are non-empty; ok=false when either is empty. Mismatched slice lengths are caller error; the kernel derives bounds from the shorter slice.

Semantics match geometry.BoundsFromXY exactly. This is the scalar back-end; the SIMD version in geom_simd.go uses simd.Float64s.Min / .Max for a ~3-4× throughput win on large input.

func CmpF64Ge

func CmpF64Ge(a []float64, b float64, out []bool)

CmpF64Ge writes out[i] = a[i] >= b for i in [0, len(a)). out must have len >= len(a); extra tail is left untouched. Callers are expected to pre-allocate out sized to len(a).

func CmpF64Gt

func CmpF64Gt(a []float64, b float64, out []bool)

CmpF64Gt writes out[i] = a[i] > b.

func CmpF64Le

func CmpF64Le(a []float64, b float64, out []bool)

CmpF64Le writes out[i] = a[i] <= b.

func CmpF64Lt

func CmpF64Lt(a []float64, b float64, out []bool)

CmpF64Lt writes out[i] = a[i] < b.

func CmpI64Ge added in v0.4.1

func CmpI64Ge(a []int64, b int64, out []bool)

CmpI64Ge writes out[i] = a[i] >= b. Same shape as CmpF64Ge but on int64 columns. Signatures match cmp_simd.go — SIMD build vectorizes the compare via simd.Int64s.GreaterEqual + mask store.

func CmpI64Gt added in v0.4.1

func CmpI64Gt(a []int64, b int64, out []bool)

CmpI64Gt writes out[i] = a[i] > b.

func CmpI64Le added in v0.4.1

func CmpI64Le(a []int64, b int64, out []bool)

CmpI64Le writes out[i] = a[i] <= b.

func CmpI64Lt added in v0.4.1

func CmpI64Lt(a []int64, b int64, out []bool)

CmpI64Lt writes out[i] = a[i] < b.

func CountTrue added in v0.4.1

func CountTrue(a []bool) int

CountTrue returns the number of true entries in a. Foundational bool-reduce kernel used by filter mask sizing (`Frame.Filter` can allocate the keep-index slice at exact selectivity instead of over-allocating to full mask length) and any bool-column count/sum reduction.

Scalar loop over `bool` values — Go's `bool` is a distinct type, not a byte to be truth-tested. The SIMD file (cmp_simd.go) shares this implementation today; if a benchmark ever justifies it, a hand-written popcount would go there.

func Enabled

func Enabled() bool

Enabled reports whether this build has SIMD kernels compiled in. True when built with `GOEXPERIMENT=simd` on arm64 or amd64; false otherwise (portable scalar fallback active). Callers don't need to branch on this — kernel dispatch happens at build time — but tests and benchmarks may want to log which path is running.

func MaxF64

func MaxF64(a []float64) (float64, bool)

MaxF64 returns (max, true) when a is non-empty, else (0, false).

func MaxI64

func MaxI64(a []int64) (int64, bool)

MaxI64 returns (max, true) when a is non-empty, else (0, false).

func MinF64

func MinF64(a []float64) (float64, bool)

MinF64 returns (min, true) when a is non-empty, else (0, false). NaN semantics match Go's built-in `<`: NaN comparisons return false, so a first-seen NaN sticks unless a later non-NaN value is strictly less. Empty groups return (0, false).

func MinI64

func MinI64(a []int64) (int64, bool)

MinI64 returns (min, true) when a is non-empty, else (0, false).

func PIPCrossingCount added in v0.4.1

func PIPCrossingCount(xs, ys []float64, tx, ty float64) bool

PIPCrossingCount is scalar in every build. Its SIMD body regressed ~2.4× on 2-lane NEON (Apple M3) and still lost 22–70% at every size on 4-lane AVX2 (Ryzen 7 5800X): with typical rings almost no segment straddles the query point's y, so the scalar branch skips nearly all the work that every vector lane pays for. PIPCrossingCount — scalar back-end. Delegates to the shared pipCrossingCountScalar helper in geom_common.go. SIMD variant with a lane-parallel body lives in geom_simd.go.

Semantics: returns the parity of ray-crossings when casting a horizontal ray from (tx, ty) rightward through the polygon ring defined by parallel Xs / Ys. inside=true when the crossing count is odd; false when even (or when the ring has fewer than 3 points).

The reformulated crossing-count form (running `crossings int` accumulator, `inside = (crossings & 1) == 1` at the tail) breaks the scalar `inside = !inside` dependency chain used in geometry.PIPRingFromXY. Output matches the AoS toggle exactly.

Handles closed and unclosed rings via the same (n-1, 0) closing-edge walk PIPRingFromXY uses.

func PolygonCentroidShoelace added in v0.4.1

func PolygonCentroidShoelace(xs, ys []float64) (cx, cy float64, ok bool)

PolygonCentroidShoelace — scalar back-end. Delegates to the shared polygonCentroidShoelaceScalar helper in geom_common.go. SIMD variant with a lane-parallel body lives in geom_simd.go.

Semantics: shoelace area-weighted centroid on the input ring. Returns (cx, cy, ok=true) when the ring has ≥3 points; (0, 0, false) when it has fewer. When areaTwo == 0 (degenerate zero-area ring) the arithmetic-mean-of-segment-starts fallback is returned via (sxFallback, syFallback, true).

Handles both closed and unclosed rings — a closing edge from (last, first) is added iff last != first.

func SumF64

func SumF64(a []float64) float64

SumF64 returns the sum of every element in a. Empty input returns 0.

func SumI64

func SumI64(a []int64) int64

SumI64 returns the sum of every element in a as int64. Overflow wraps (matches Go's built-in `+` semantics). Empty input returns 0.

func WithinSqDistF64

func WithinSqDistF64(lats, lons []float64, refLat, refLon, cosRefLat, sqThreshold float64, out []bool)

WithinSqDistF64 writes

out[i] = ((lats[i]-refLat)² + ((lons[i]-refLon)·cosRefLat)²) <= sqThreshold

— the equirectangular-approximation "point within radius r of (refLat, refLon)" filter. Compute-heavy shape: 2 subtractions + 2 squarings + 1 multiply-by-scaling + 1 add + 1 compare per row, all fully vectorizable. Callers precompute cosRefLat once (avoids per-row trig) and pass the squared threshold to save a sqrt in the inner loop.

Accuracy: equirect approximation is fine for distances small relative to Earth's radius (< a few hundred km). For global distances use a proper haversine impl (not SIMD-friendly on the current simd.Float64s surface — no atan2/trig).

lats and lons must have equal length; out must have len >= len(lats).

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL