blob: 9f7f17d14bcd0d69dd0f1face9f19b0d98e87e5e [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build goexperiment.simd && arm64
package simd_test
import (
"fmt"
"simd/archsimd"
"strings"
"testing"
)
func TestLookupOrZero(t *testing.T) {
// Out-of-range indices produce zero lane value.
x := []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
indices := []uint8{7, 6, 5, 4, 3, 2, 1, 0, 0xff, 8, 16, 9, 128, 10, 20, 11}
want := []uint8{8, 7, 6, 5, 4, 3, 2, 1, 0, 9, 0, 10, 0, 11, 0, 12}
got := make([]uint8, len(x))
archsimd.LoadUint8x16(x).LookupOrZero(archsimd.LoadUint8x16(indices)).StorePart(got)
checkSlices(t, got, want)
}
func TestClMul(t *testing.T) {
if !archsimd.ARM64.PMULL() {
t.Skip("no carryless multiply")
}
var x = archsimd.LoadUint64x2([]uint64{1, 5})
var y = archsimd.LoadUint64x2([]uint64{3, 9})
foo := func(v archsimd.Uint64x2, s []uint64) {
r := make([]uint64, 2, 2)
v.StorePart(r)
checkSlices[uint64](t, r, s)
}
foo(x.CarrylessMultiplyEven(y), []uint64{3, 0})
foo(x.CarrylessMultiplyEvenOdd(y), []uint64{9, 0})
foo(x.CarrylessMultiplyOddEven(y), []uint64{15, 0})
foo(x.CarrylessMultiplyOdd(y), []uint64{45, 0})
foo(y.CarrylessMultiplyEven(y), []uint64{5, 0})
}
//go:noinline
func addInt8sNoinline(a, b archsimd.Int8s) archsimd.Int8s { return a.Add(b) }
//go:noinline
func blackholeSVE() {}
// TestAddSVEAcrossCall passes scalable vectors across a real (non-inlined) ABI
// boundary, exercising the register/stack passing that size.go's simdify decides
// for SVE types.
func TestAddSVEAcrossCall(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
var a, b, got [32]int8
for i := range a {
a[i] = int8(i)
b[i] = int8(2*i + 1)
}
x := archsimd.LoadInt8s(a[:])
addInt8sNoinline(x, archsimd.LoadInt8s(b[:])).Store(got[:])
for i := 0; i < x.Len(); i++ {
if want := a[i] + b[i]; got[i] != want {
t.Errorf("lane %d: got %d, want %d", i, got[i], want)
}
}
}
//go:noinline
func greaterInt8sNoinline(a, b archsimd.Int8s) archsimd.Mask8s { return a.Greater(b) }
// TestGreaterSVEMaskRoundTrip returns a mask across a non-inlined call, exercising
// the predicate memory round-trip (PSTR to return it, PLDR to reload it) that the
// mask ABI relies on, and checks it against a > b lane by lane.
func TestGreaterSVEMaskRoundTrip(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
var a, b [32]int8
for i := range a {
a[i] = int8(i - 8)
b[i] = int8(2*i - 20)
}
var z archsimd.Int8s
m := greaterInt8sNoinline(archsimd.LoadInt8s(a[:]), archsimd.LoadInt8s(b[:]))
bits := maskBits(m)
for i := 0; i < z.Len(); i++ {
want := a[i] > b[i]
got := bits>>i&1 == 1
if got != want {
t.Errorf("lane %d: got %v, want %v (a=%d b=%d)", i, got, want, a[i], b[i])
}
}
if bits>>z.Len() != 0 {
t.Errorf("bits beyond the vector length are set: %#x", bits)
}
}
// TestAddSVESpill keeps a scalable vector live across a call, forcing the
// register allocator to spill and reload it (ZSTR/ZLDR).
func TestAddSVESpill(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
var a, b, got [32]int8
for i := range a {
a[i] = int8(i)
b[i] = int8(100 - i)
}
sum := archsimd.LoadInt8s(a[:]).Add(archsimd.LoadInt8s(b[:]))
blackholeSVE() // clobbers caller-saved regs; sum must survive via a spill
sum.Store(got[:])
for i := 0; i < sum.Len(); i++ {
if want := a[i] + b[i]; got[i] != want {
t.Errorf("lane %d: got %d, want %d", i, got[i], want)
}
}
}
// TestAddSaturatedSVE checks the generated saturating add: an explicit
// boundary case, then every integer shape against the generic emulation.
func TestAddSaturatedSVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
var si, gi [32]int8
for i := range si {
si[i] = 100 // 100+100 saturates to +127
}
vi := archsimd.LoadInt8s(si[:])
vi.AddSaturated(vi).Store(gi[:])
for i := 0; i < vi.Len(); i++ {
if gi[i] != 127 {
t.Errorf("int8 lane %d: got %d, want 127", i, gi[i])
}
}
testInt8sBinary(t, archsimd.Int8s.AddSaturated, addSaturatedSlice[int8])
testInt16sBinary(t, archsimd.Int16s.AddSaturated, addSaturatedSlice[int16])
testInt32sBinary(t, archsimd.Int32s.AddSaturated, addSaturatedSlice[int32])
testInt64sBinary(t, archsimd.Int64s.AddSaturated, addSaturatedSlice[int64])
testUint8sBinary(t, archsimd.Uint8s.AddSaturated, addSaturatedSlice[uint8])
testUint16sBinary(t, archsimd.Uint16s.AddSaturated, addSaturatedSlice[uint16])
testUint32sBinary(t, archsimd.Uint32s.AddSaturated, addSaturatedSlice[uint32])
testUint64sBinary(t, archsimd.Uint64s.AddSaturated, addSaturatedSlice[uint64])
}
// TestSubSaturatedSVE checks the generated saturating subtract: an explicit
// boundary case, then every integer shape against the generic emulation.
func TestSubSaturatedSVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
var sx, sy, gi [32]int8
for i := range sx {
sx[i] = 100 // 100 - (-100) saturates to +127
sy[i] = -100
}
x, y := archsimd.LoadInt8s(sx[:]), archsimd.LoadInt8s(sy[:])
x.SubSaturated(y).Store(gi[:])
for i := 0; i < x.Len(); i++ {
if gi[i] != 127 {
t.Errorf("int8 lane %d: got %d, want 127", i, gi[i])
}
}
testInt8sBinary(t, archsimd.Int8s.SubSaturated, subSaturatedSlice[int8])
testInt16sBinary(t, archsimd.Int16s.SubSaturated, subSaturatedSlice[int16])
testInt32sBinary(t, archsimd.Int32s.SubSaturated, subSaturatedSlice[int32])
testInt64sBinary(t, archsimd.Int64s.SubSaturated, subSaturatedSlice[int64])
testUint8sBinary(t, archsimd.Uint8s.SubSaturated, subSaturatedSlice[uint8])
testUint16sBinary(t, archsimd.Uint16s.SubSaturated, subSaturatedSlice[uint16])
testUint32sBinary(t, archsimd.Uint32s.SubSaturated, subSaturatedSlice[uint32])
testUint64sBinary(t, archsimd.Uint64s.SubSaturated, subSaturatedSlice[uint64])
}
func TestStringSVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
want := func(v any) string {
return "{" + strings.ReplaceAll(strings.Trim(fmt.Sprint(v), "[]"), " ", ",") + "}"
}
xs := make([]int8, archsimd.Int8s{}.Len())
ys := make([]int64, archsimd.Int64s{}.Len())
for i := range xs {
xs[i] = int8(i % 2)
}
for i := range ys {
ys[i] = int64(i % 2)
}
x := archsimd.LoadInt8s(xs)
y := archsimd.LoadInt64s(ys)
mx := x.Greater(archsimd.LoadInt8s(make([]int8, len(xs))))
my := y.Greater(archsimd.LoadInt64s(make([]int64, len(ys))))
if x.String() != want(xs) {
t.Errorf("x=%s wanted %s", x, want(xs))
}
if y.String() != want(ys) {
t.Errorf("y=%s wanted %s", y, want(ys))
}
if mx.String() != want(xs) {
t.Errorf("mx=%s wanted %s", mx, want(xs))
}
if my.String() != want(ys) {
t.Errorf("my=%s wanted %s", my, want(ys))
}
t.Logf("x=%s", x)
t.Logf("y=%s", y)
t.Logf("mx=%s", mx)
t.Logf("my=%s", my)
}
//go:noinline
func keepAliveInt8s(archsimd.Int8s) {}
// namedMask8s and maxVia pass masks through types other than archsimd.Mask8s
// itself: a defined type, and the GC shape of a generic instantiation. Both
// must use the memory ABI of the mask they are made from, to agree with the
// concrete functions they call and to reach a P register when used.
type namedMask8s archsimd.Mask8s
//go:noinline
func greaterNamed(x, y archsimd.Int8s) namedMask8s { return namedMask8s(x.Greater(y)) }
//go:noinline
func maxVia[M archsimd.Mask8s](greater func(x, y archsimd.Int8s) M, x, y archsimd.Int8s) archsimd.Int8s {
return x.IfElse(archsimd.Mask8s(greater(x, y)), y)
}
func TestMaskABISVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
n := archsimd.Int8s{}.Len()
xs, ys := make([]int8, n), make([]int8, n)
for i := range xs {
xs[i], ys[i] = int8(i%5), int8(i%3)
}
x, y := archsimd.LoadInt8s(xs), archsimd.LoadInt8s(ys)
check := func(name string, v archsimd.Int8s) {
t.Helper()
got := make([]int8, n)
v.Store(got)
for i := range got {
if want := max(xs[i], ys[i]); got[i] != want {
t.Errorf("%s: lane %d = %d, want %d", name, i, got[i], want)
}
}
}
check("generic", maxVia(archsimd.Int8s.Greater, x, y))
check("named", x.IfElse(archsimd.Mask8s(greaterNamed(x, y)), y))
}
// TestIfElseSVE checks IfElse and Masked, and that the merging peephole keeps
// the same semantics whether or not it fires: x.Add(y).IfElse(m, x) folds into a
// predicated add, x.Add(y).IfElse(m, z) does not, and both must agree with a
// lane-by-lane reference.
func TestIfElseSVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
n := archsimd.Int8s{}.Len()
xs, ys, zs := make([]int8, n), make([]int8, n), make([]int8, n)
for i := range xs {
xs[i] = int8(i + 1)
ys[i] = int8(i % 3) // active where xs[i] > ys[i], which alternates early on
zs[i] = int8(-i - 1)
}
x, y, z := archsimd.LoadInt8s(xs), archsimd.LoadInt8s(ys), archsimd.LoadInt8s(zs)
m := x.Greater(y)
got := make([]int8, n)
check := func(name string, v archsimd.Int8s, want func(i int, active bool) int8) {
t.Helper()
v.Store(got)
for i := 0; i < n; i++ {
if w := want(i, xs[i] > ys[i]); got[i] != w {
t.Errorf("%s: lane %d = %d, want %d (x=%d y=%d)", name, i, got[i], w, xs[i], ys[i])
}
}
}
check("IfElse", x.IfElse(m, y), func(i int, active bool) int8 {
if active {
return xs[i]
}
return ys[i]
})
check("Masked", x.Masked(m), func(i int, active bool) int8 {
if active {
return xs[i]
}
return 0
})
// Folds into the merging-predicated add.
check("Add.IfElse(x)", x.Add(y).IfElse(m, x), func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return xs[i]
})
// Folds via commutativity.
check("Add.IfElse(y)", x.Add(y).IfElse(m, y), func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return ys[i]
})
// Folds behind a merging MOVPRFX: the else operand is neither source.
check("Add.IfElse(z)", x.Add(y).IfElse(m, z), func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return zs[i]
})
// ADD has no zeroing-predicated form, so Masked folds into the merging one
// with the zero vector as its else operand.
check("Add.Masked", x.Add(y).Masked(m), func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return 0
})
// The two merges below differ only in which source the inactive lanes
// keep. The merging machine op pins its result to that source and is not
// commutative, or CSE — which canonicalizes the args of a commutative op —
// would conflate the two values and give one of them the wrong else
// operand.
keepX := x.Add(y).IfElse(m, x)
keepY := y.Add(x).IfElse(m, y)
check("Add.IfElse cse keepX", keepX, func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return xs[i]
})
check("Add.IfElse cse keepY", keepY, func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return ys[i]
})
// SUB is not commutative, so only an "else" operand that is the destructive
// one — the minuend — folds into the merging-predicated instruction.
check("Sub.IfElse(x)", x.Sub(y).IfElse(m, x), func(i int, active bool) int8 {
if active {
return xs[i] - ys[i]
}
return xs[i]
})
// Does not fold, and must not silently become y-x.
check("Sub.IfElse(y)", x.Sub(y).IfElse(m, y), func(i int, active bool) int8 {
if active {
return xs[i] - ys[i]
}
return ys[i]
})
// Does not fold: there is no prefixed form for a non-commutative operation.
check("Sub.IfElse(z)", x.Sub(y).IfElse(m, z), func(i int, active bool) int8 {
if active {
return xs[i] - ys[i]
}
return zs[i]
})
check("Sub.Masked", x.Sub(y).Masked(m), func(i int, active bool) int8 {
if active {
return xs[i] - ys[i]
}
return 0
})
// Abs is predicated-only, so its unpredicated API runs under an all-true
// predicate that a select can simply replace: IfElse becomes the merging
// form and Masked the zeroing one, each a single instruction.
absLane := func(v int8) int8 {
if v < 0 {
return -v
}
return v
}
check("Abs.IfElse(z)", x.Abs().IfElse(m, z), func(i int, active bool) int8 {
if active {
return absLane(xs[i])
}
return zs[i]
})
check("Abs.IfElse(x)", x.Abs().IfElse(m, x), func(i int, active bool) int8 {
if active {
return absLane(xs[i])
}
return xs[i]
})
check("Abs.Masked", x.Abs().Masked(m), func(i int, active bool) int8 {
if active {
return absLane(xs[i])
}
return 0
})
// The prefixed path with every operand still live afterwards, so the
// destination can be none of them and the merging MOVPRFX has to place the
// else operand itself.
rz := x.Add(y).IfElse(m, z)
keepAliveInt8s(x)
keepAliveInt8s(y)
keepAliveInt8s(z)
check("Add.IfElse(z) with all live", rz, func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return zs[i]
})
// The MOVPRFX path: x must survive the destructive predicated add.
r := x.Add(y).IfElse(m, x)
keepAliveInt8s(x)
check("Add.IfElse(x) with x live", r, func(i int, active bool) int8 {
if active {
return xs[i] + ys[i]
}
return xs[i]
})
x.Store(got)
for i := 0; i < n; i++ {
if got[i] != xs[i] {
t.Errorf("x clobbered by MOVPRFX: lane %d = %d, want %d", i, got[i], xs[i])
}
}
}
// TestMaskAllTrueSVE checks the all-true mask constructors: a select under an
// all-true mask returns its first operand in every lane, at each lane width.
func TestMaskAllTrueSVE(t *testing.T) {
if !archsimd.ARM64.SVE() {
t.Skip("no sve")
}
check := func(name string, n int, sel func(i int) (got, want any)) {
t.Helper()
for i := 0; i < n; i++ {
if got, want := sel(i); got != want {
t.Errorf("%s: lane %d = %v, want %v", name, i, got, want)
}
}
}
xs8, ys8 := make([]int8, archsimd.Int8s{}.Len()), make([]int8, archsimd.Int8s{}.Len())
for i := range xs8 {
xs8[i], ys8[i] = int8(i+1), int8(-i-1)
}
g8 := make([]int8, len(xs8))
archsimd.LoadInt8s(xs8).IfElse(archsimd.Mask8sAllTrue(), archsimd.LoadInt8s(ys8)).Store(g8)
check("Mask8sAllTrue", len(xs8), func(i int) (any, any) { return g8[i], xs8[i] })
xs16, ys16 := make([]int16, archsimd.Int16s{}.Len()), make([]int16, archsimd.Int16s{}.Len())
for i := range xs16 {
xs16[i], ys16[i] = int16(i+1), int16(-i-1)
}
g16 := make([]int16, len(xs16))
archsimd.LoadInt16s(xs16).IfElse(archsimd.Mask16sAllTrue(), archsimd.LoadInt16s(ys16)).Store(g16)
check("Mask16sAllTrue", len(xs16), func(i int) (any, any) { return g16[i], xs16[i] })
xs32, ys32 := make([]int32, archsimd.Int32s{}.Len()), make([]int32, archsimd.Int32s{}.Len())
for i := range xs32 {
xs32[i], ys32[i] = int32(i+1), int32(-i-1)
}
g32 := make([]int32, len(xs32))
archsimd.LoadInt32s(xs32).IfElse(archsimd.Mask32sAllTrue(), archsimd.LoadInt32s(ys32)).Store(g32)
check("Mask32sAllTrue", len(xs32), func(i int) (any, any) { return g32[i], xs32[i] })
xs64, ys64 := make([]int64, archsimd.Int64s{}.Len()), make([]int64, archsimd.Int64s{}.Len())
for i := range xs64 {
xs64[i], ys64[i] = int64(i+1), int64(-i-1)
}
g64 := make([]int64, len(xs64))
archsimd.LoadInt64s(xs64).IfElse(archsimd.Mask64sAllTrue(), archsimd.LoadInt64s(ys64)).Store(g64)
check("Mask64sAllTrue", len(xs64), func(i int) (any, any) { return g64[i], xs64[i] })
}