| // 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 && (amd64 || arm64 || wasm) | |
| package simd_test | |
| import ( | |
| "math" | |
| "runtime" | |
| "simd/archsimd" | |
| "testing" | |
| ) | |
| func TestFloatMinMax32_128(t *testing.T) { | |
| testFloatMinMax32(t, 4, floatMinMax32x4) | |
| } | |
| func TestFloatMinMax64_128(t *testing.T) { | |
| testFloatMinMax64(t, 2, floatMinMax64x2) | |
| } | |
| func testFloatMinMax32(t *testing.T, lanes int, run func([]float32, []float32, [4][]float32, bool, bool)) { | |
| t.Helper() | |
| fromBits := func(x uint64) float32 { return math.Float32frombits(uint32(x)) } | |
| toBits := func(x float32) uint64 { return uint64(math.Float32bits(x)) } | |
| testFloatMinMax(t, lanes, floatMinMax32Cases(), fromBits, toBits, run, floatMinMaxPlatformConfig()) | |
| } | |
| func testFloatMinMax64(t *testing.T, lanes int, run func([]float64, []float64, [4][]float64, bool, bool)) { | |
| t.Helper() | |
| testFloatMinMax(t, lanes, floatMinMax64Cases(), math.Float64frombits, math.Float64bits, run, floatMinMaxPlatformConfig()) | |
| } | |
| // AMD64 selects the second operand for NaNs and equal zeros. ARM64 propagates | |
| // NaNs, preferring the first operand when both are quiet NaNs. Wasm uses the | |
| // ARM64 expectations for numbers and signed zeros, but permits any NaN payload. | |
| type floatMinMaxCase struct { | |
| name string | |
| x, y uint64 | |
| wantAMD64, wantARM64 [4]uint64 // Min(x,y), Min(y,x), Max(x,y), Max(y,x) | |
| } | |
| func floatMinMax32Cases() []floatMinMaxCase { | |
| const ( | |
| one = 0x3f800000 | |
| two = 0x40000000 | |
| negZero = 0x80000000 | |
| qnan1 = 0x7fc00001 | |
| qnan2 = 0x7fc00002 | |
| snan = 0x7f800001 | |
| inf = 0x7f800000 | |
| negInf = 0xff800000 | |
| ) | |
| return []floatMinMaxCase{ | |
| {"NaN-number", qnan1, one, [4]uint64{one, qnan1, one, qnan1}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"number-NaN", one, qnan1, [4]uint64{qnan1, one, qnan1, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"NaN-payloads", qnan1, qnan2, [4]uint64{qnan2, qnan1, qnan2, qnan1}, [4]uint64{qnan1, qnan2, qnan1, qnan2}}, | |
| {"sNaN-number", snan, one, [4]uint64{one, snan, one, snan}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"number-sNaN", one, snan, [4]uint64{snan, one, snan, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"negative-zero", negZero, 0, [4]uint64{0, negZero, 0, negZero}, [4]uint64{negZero, negZero, 0, 0}}, | |
| {"positive-zero", 0, negZero, [4]uint64{negZero, 0, negZero, 0}, [4]uint64{negZero, negZero, 0, 0}}, | |
| {"finite", two, one, [4]uint64{one, one, two, two}, [4]uint64{one, one, two, two}}, | |
| {"infinities", inf, negInf, [4]uint64{negInf, negInf, inf, inf}, [4]uint64{negInf, negInf, inf, inf}}, | |
| } | |
| } | |
| func floatMinMax64Cases() []floatMinMaxCase { | |
| const ( | |
| one = 0x3ff0000000000000 | |
| two = 0x4000000000000000 | |
| negZero = 0x8000000000000000 | |
| qnan1 = 0x7ff8000000000001 | |
| qnan2 = 0x7ff8000000000002 | |
| snan = 0x7ff0000000000001 | |
| inf = 0x7ff0000000000000 | |
| negInf = 0xfff0000000000000 | |
| ) | |
| return []floatMinMaxCase{ | |
| {"NaN-number", qnan1, one, [4]uint64{one, qnan1, one, qnan1}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"number-NaN", one, qnan1, [4]uint64{qnan1, one, qnan1, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"NaN-payloads", qnan1, qnan2, [4]uint64{qnan2, qnan1, qnan2, qnan1}, [4]uint64{qnan1, qnan2, qnan1, qnan2}}, | |
| {"sNaN-number", snan, one, [4]uint64{one, snan, one, snan}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"number-sNaN", one, snan, [4]uint64{snan, one, snan, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}}, | |
| {"negative-zero", negZero, 0, [4]uint64{0, negZero, 0, negZero}, [4]uint64{negZero, negZero, 0, 0}}, | |
| {"positive-zero", 0, negZero, [4]uint64{negZero, 0, negZero, 0}, [4]uint64{negZero, negZero, 0, 0}}, | |
| {"finite", two, one, [4]uint64{one, one, two, two}, [4]uint64{one, one, two, two}}, | |
| {"infinities", inf, negInf, [4]uint64{negInf, negInf, inf, inf}, [4]uint64{negInf, negInf, inf, inf}}, | |
| } | |
| } | |
| type floatMinMaxConfig struct { | |
| allowMasked bool | |
| allowAnyNaN bool | |
| } | |
| func testFloatMinMax[T float](t *testing.T, lanes int, cases []floatMinMaxCase, | |
| fromBits func(uint64) T, toBits func(T) uint64, | |
| run func([]T, []T, [4][]T, bool, bool), config floatMinMaxConfig) { | |
| t.Helper() | |
| for _, form := range []struct { | |
| name string | |
| reverse, masked bool | |
| }{ | |
| {"forward", false, false}, | |
| {"reverse", true, false}, | |
| {"masked-forward", false, true}, | |
| {"masked-reverse", true, true}, | |
| } { | |
| t.Run(form.name, func(t *testing.T) { | |
| if form.masked && !config.allowMasked { | |
| t.Skip("requires AVX512") | |
| } | |
| for _, c := range cases { | |
| t.Run(c.name, func(t *testing.T) { | |
| wantResults := c.wantARM64 | |
| if runtime.GOARCH == "amd64" { | |
| wantResults = c.wantAMD64 | |
| } | |
| x, y := make([]T, lanes), make([]T, lanes) | |
| var out [4][]T | |
| for i := range out { | |
| out[i] = make([]T, lanes) | |
| } | |
| for i := range x { | |
| x[i], y[i] = fromBits(c.x), fromBits(c.y) | |
| } | |
| run(x, y, out, form.reverse, form.masked) | |
| for j, name := range []string{"Min(x,y)", "Min(y,x)", "Max(x,y)", "Max(y,x)"} { | |
| for i, v := range out[j] { | |
| want := wantResults[j] | |
| if form.masked && i%2 != 0 { | |
| want = 0 | |
| } | |
| if config.allowAnyNaN && math.IsNaN(float64(fromBits(want))) { | |
| if !math.IsNaN(float64(v)) { | |
| t.Errorf("%s lane %d: got %#x, want NaN", name, i, toBits(v)) | |
| } | |
| continue | |
| } | |
| if got := toBits(v); got != want { | |
| t.Errorf("%s lane %d: got %#x, want %#x", name, i, got, want) | |
| } | |
| } | |
| } | |
| }) | |
| } | |
| }) | |
| } | |
| } | |
| // Compute both operand orders in one function to exercise CSE. | |
| // | |
| //go:noinline | |
| func floatMinMax32x4(a, b []float32, out [4][]float32, reverse, masked bool) { | |
| x, y := archsimd.LoadFloat32x4(a), archsimd.LoadFloat32x4(b) | |
| var minXY, minYX, maxXY, maxYX archsimd.Float32x4 | |
| if masked { | |
| mask := floatMinMaxMask32x4() | |
| if reverse { | |
| minYX, minXY = y.Min(x).Masked(mask), x.Min(y).Masked(mask) | |
| maxYX, maxXY = y.Max(x).Masked(mask), x.Max(y).Masked(mask) | |
| } else { | |
| minXY, minYX = x.Min(y).Masked(mask), y.Min(x).Masked(mask) | |
| maxXY, maxYX = x.Max(y).Masked(mask), y.Max(x).Masked(mask) | |
| } | |
| } else if reverse { | |
| minYX, minXY = y.Min(x), x.Min(y) | |
| maxYX, maxXY = y.Max(x), x.Max(y) | |
| } else { | |
| minXY, minYX = x.Min(y), y.Min(x) | |
| maxXY, maxYX = x.Max(y), y.Max(x) | |
| } | |
| minXY.Store(out[0]) | |
| minYX.Store(out[1]) | |
| maxXY.Store(out[2]) | |
| maxYX.Store(out[3]) | |
| } | |
| //go:noinline | |
| func floatMinMax64x2(a, b []float64, out [4][]float64, reverse, masked bool) { | |
| x, y := archsimd.LoadFloat64x2(a), archsimd.LoadFloat64x2(b) | |
| var minXY, minYX, maxXY, maxYX archsimd.Float64x2 | |
| if masked { | |
| mask := floatMinMaxMask64x2() | |
| if reverse { | |
| minYX, minXY = y.Min(x).Masked(mask), x.Min(y).Masked(mask) | |
| maxYX, maxXY = y.Max(x).Masked(mask), x.Max(y).Masked(mask) | |
| } else { | |
| minXY, minYX = x.Min(y).Masked(mask), y.Min(x).Masked(mask) | |
| maxXY, maxYX = x.Max(y).Masked(mask), y.Max(x).Masked(mask) | |
| } | |
| } else if reverse { | |
| minYX, minXY = y.Min(x), x.Min(y) | |
| maxYX, maxXY = y.Max(x), x.Max(y) | |
| } else { | |
| minXY, minYX = x.Min(y), y.Min(x) | |
| maxXY, maxYX = x.Max(y), y.Max(x) | |
| } | |
| minXY.Store(out[0]) | |
| minYX.Store(out[1]) | |
| maxXY.Store(out[2]) | |
| maxYX.Store(out[3]) | |
| } |