| // 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]) |
| } |