Source file src/simd/archsimd/internal/simd_test/minmax_test.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build goexperiment.simd && (amd64 || arm64 || wasm)
     6  
     7  package simd_test
     8  
     9  import (
    10  	"math"
    11  	"runtime"
    12  	"simd/archsimd"
    13  	"testing"
    14  )
    15  
    16  func TestFloatMinMax32_128(t *testing.T) {
    17  	testFloatMinMax32(t, 4, floatMinMax32x4)
    18  }
    19  
    20  func TestFloatMinMax64_128(t *testing.T) {
    21  	testFloatMinMax64(t, 2, floatMinMax64x2)
    22  }
    23  
    24  func testFloatMinMax32(t *testing.T, lanes int, run func([]float32, []float32, [4][]float32, bool, bool)) {
    25  	t.Helper()
    26  	fromBits := func(x uint64) float32 { return math.Float32frombits(uint32(x)) }
    27  	toBits := func(x float32) uint64 { return uint64(math.Float32bits(x)) }
    28  	testFloatMinMax(t, lanes, floatMinMax32Cases(), fromBits, toBits, run, floatMinMaxPlatformConfig())
    29  }
    30  
    31  func testFloatMinMax64(t *testing.T, lanes int, run func([]float64, []float64, [4][]float64, bool, bool)) {
    32  	t.Helper()
    33  	testFloatMinMax(t, lanes, floatMinMax64Cases(), math.Float64frombits, math.Float64bits, run, floatMinMaxPlatformConfig())
    34  }
    35  
    36  // AMD64 selects the second operand for NaNs and equal zeros. ARM64 propagates
    37  // NaNs, preferring the first operand when both are quiet NaNs. Wasm uses the
    38  // ARM64 expectations for numbers and signed zeros, but permits any NaN payload.
    39  type floatMinMaxCase struct {
    40  	name                 string
    41  	x, y                 uint64
    42  	wantAMD64, wantARM64 [4]uint64 // Min(x,y), Min(y,x), Max(x,y), Max(y,x)
    43  }
    44  
    45  func floatMinMax32Cases() []floatMinMaxCase {
    46  	const (
    47  		one     = 0x3f800000
    48  		two     = 0x40000000
    49  		negZero = 0x80000000
    50  		qnan1   = 0x7fc00001
    51  		qnan2   = 0x7fc00002
    52  		snan    = 0x7f800001
    53  		inf     = 0x7f800000
    54  		negInf  = 0xff800000
    55  	)
    56  	return []floatMinMaxCase{
    57  		{"NaN-number", qnan1, one, [4]uint64{one, qnan1, one, qnan1}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    58  		{"number-NaN", one, qnan1, [4]uint64{qnan1, one, qnan1, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    59  		{"NaN-payloads", qnan1, qnan2, [4]uint64{qnan2, qnan1, qnan2, qnan1}, [4]uint64{qnan1, qnan2, qnan1, qnan2}},
    60  		{"sNaN-number", snan, one, [4]uint64{one, snan, one, snan}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    61  		{"number-sNaN", one, snan, [4]uint64{snan, one, snan, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    62  		{"negative-zero", negZero, 0, [4]uint64{0, negZero, 0, negZero}, [4]uint64{negZero, negZero, 0, 0}},
    63  		{"positive-zero", 0, negZero, [4]uint64{negZero, 0, negZero, 0}, [4]uint64{negZero, negZero, 0, 0}},
    64  		{"finite", two, one, [4]uint64{one, one, two, two}, [4]uint64{one, one, two, two}},
    65  		{"infinities", inf, negInf, [4]uint64{negInf, negInf, inf, inf}, [4]uint64{negInf, negInf, inf, inf}},
    66  	}
    67  }
    68  
    69  func floatMinMax64Cases() []floatMinMaxCase {
    70  	const (
    71  		one     = 0x3ff0000000000000
    72  		two     = 0x4000000000000000
    73  		negZero = 0x8000000000000000
    74  		qnan1   = 0x7ff8000000000001
    75  		qnan2   = 0x7ff8000000000002
    76  		snan    = 0x7ff0000000000001
    77  		inf     = 0x7ff0000000000000
    78  		negInf  = 0xfff0000000000000
    79  	)
    80  	return []floatMinMaxCase{
    81  		{"NaN-number", qnan1, one, [4]uint64{one, qnan1, one, qnan1}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    82  		{"number-NaN", one, qnan1, [4]uint64{qnan1, one, qnan1, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    83  		{"NaN-payloads", qnan1, qnan2, [4]uint64{qnan2, qnan1, qnan2, qnan1}, [4]uint64{qnan1, qnan2, qnan1, qnan2}},
    84  		{"sNaN-number", snan, one, [4]uint64{one, snan, one, snan}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    85  		{"number-sNaN", one, snan, [4]uint64{snan, one, snan, one}, [4]uint64{qnan1, qnan1, qnan1, qnan1}},
    86  		{"negative-zero", negZero, 0, [4]uint64{0, negZero, 0, negZero}, [4]uint64{negZero, negZero, 0, 0}},
    87  		{"positive-zero", 0, negZero, [4]uint64{negZero, 0, negZero, 0}, [4]uint64{negZero, negZero, 0, 0}},
    88  		{"finite", two, one, [4]uint64{one, one, two, two}, [4]uint64{one, one, two, two}},
    89  		{"infinities", inf, negInf, [4]uint64{negInf, negInf, inf, inf}, [4]uint64{negInf, negInf, inf, inf}},
    90  	}
    91  }
    92  
    93  type floatMinMaxConfig struct {
    94  	allowMasked bool
    95  	allowAnyNaN bool
    96  }
    97  
    98  func testFloatMinMax[T float](t *testing.T, lanes int, cases []floatMinMaxCase,
    99  	fromBits func(uint64) T, toBits func(T) uint64,
   100  	run func([]T, []T, [4][]T, bool, bool), config floatMinMaxConfig) {
   101  	t.Helper()
   102  	for _, form := range []struct {
   103  		name            string
   104  		reverse, masked bool
   105  	}{
   106  		{"forward", false, false},
   107  		{"reverse", true, false},
   108  		{"masked-forward", false, true},
   109  		{"masked-reverse", true, true},
   110  	} {
   111  		t.Run(form.name, func(t *testing.T) {
   112  			if form.masked && !config.allowMasked {
   113  				t.Skip("requires AVX512")
   114  			}
   115  			for _, c := range cases {
   116  				t.Run(c.name, func(t *testing.T) {
   117  					wantResults := c.wantARM64
   118  					if runtime.GOARCH == "amd64" {
   119  						wantResults = c.wantAMD64
   120  					}
   121  					x, y := make([]T, lanes), make([]T, lanes)
   122  					var out [4][]T
   123  					for i := range out {
   124  						out[i] = make([]T, lanes)
   125  					}
   126  					for i := range x {
   127  						x[i], y[i] = fromBits(c.x), fromBits(c.y)
   128  					}
   129  					run(x, y, out, form.reverse, form.masked)
   130  					for j, name := range []string{"Min(x,y)", "Min(y,x)", "Max(x,y)", "Max(y,x)"} {
   131  						for i, v := range out[j] {
   132  							want := wantResults[j]
   133  							if form.masked && i%2 != 0 {
   134  								want = 0
   135  							}
   136  							if config.allowAnyNaN && math.IsNaN(float64(fromBits(want))) {
   137  								if !math.IsNaN(float64(v)) {
   138  									t.Errorf("%s lane %d: got %#x, want NaN", name, i, toBits(v))
   139  								}
   140  								continue
   141  							}
   142  							if got := toBits(v); got != want {
   143  								t.Errorf("%s lane %d: got %#x, want %#x", name, i, got, want)
   144  							}
   145  						}
   146  					}
   147  				})
   148  			}
   149  		})
   150  	}
   151  }
   152  
   153  // Compute both operand orders in one function to exercise CSE.
   154  //
   155  //go:noinline
   156  func floatMinMax32x4(a, b []float32, out [4][]float32, reverse, masked bool) {
   157  	x, y := archsimd.LoadFloat32x4(a), archsimd.LoadFloat32x4(b)
   158  	var minXY, minYX, maxXY, maxYX archsimd.Float32x4
   159  	if masked {
   160  		mask := floatMinMaxMask32x4()
   161  		if reverse {
   162  			minYX, minXY = y.Min(x).Masked(mask), x.Min(y).Masked(mask)
   163  			maxYX, maxXY = y.Max(x).Masked(mask), x.Max(y).Masked(mask)
   164  		} else {
   165  			minXY, minYX = x.Min(y).Masked(mask), y.Min(x).Masked(mask)
   166  			maxXY, maxYX = x.Max(y).Masked(mask), y.Max(x).Masked(mask)
   167  		}
   168  	} else if reverse {
   169  		minYX, minXY = y.Min(x), x.Min(y)
   170  		maxYX, maxXY = y.Max(x), x.Max(y)
   171  	} else {
   172  		minXY, minYX = x.Min(y), y.Min(x)
   173  		maxXY, maxYX = x.Max(y), y.Max(x)
   174  	}
   175  	minXY.Store(out[0])
   176  	minYX.Store(out[1])
   177  	maxXY.Store(out[2])
   178  	maxYX.Store(out[3])
   179  }
   180  
   181  //go:noinline
   182  func floatMinMax64x2(a, b []float64, out [4][]float64, reverse, masked bool) {
   183  	x, y := archsimd.LoadFloat64x2(a), archsimd.LoadFloat64x2(b)
   184  	var minXY, minYX, maxXY, maxYX archsimd.Float64x2
   185  	if masked {
   186  		mask := floatMinMaxMask64x2()
   187  		if reverse {
   188  			minYX, minXY = y.Min(x).Masked(mask), x.Min(y).Masked(mask)
   189  			maxYX, maxXY = y.Max(x).Masked(mask), x.Max(y).Masked(mask)
   190  		} else {
   191  			minXY, minYX = x.Min(y).Masked(mask), y.Min(x).Masked(mask)
   192  			maxXY, maxYX = x.Max(y).Masked(mask), y.Max(x).Masked(mask)
   193  		}
   194  	} else if reverse {
   195  		minYX, minXY = y.Min(x), x.Min(y)
   196  		maxYX, maxXY = y.Max(x), x.Max(y)
   197  	} else {
   198  		minXY, minYX = x.Min(y), y.Min(x)
   199  		maxXY, maxYX = x.Max(y), y.Max(x)
   200  	}
   201  	minXY.Store(out[0])
   202  	minYX.Store(out[1])
   203  	maxXY.Store(out[2])
   204  	maxYX.Store(out[3])
   205  }
   206  

View as plain text