github.com/grailbio/base@v0.0.11/simd/float_test.go (about) 1 // Copyright 2021 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package simd_test 6 7 import ( 8 "math" 9 "math/rand" 10 "testing" 11 12 "github.com/grailbio/base/simd" 13 "github.com/grailbio/testutil/expect" 14 ) 15 16 func findNaNOrInf64Standard(data []float64) int { 17 for i, x := range data { 18 if math.IsNaN(x) || (x > math.MaxFloat64) || (x < -math.MaxFloat64) { 19 return i 20 } 21 } 22 return -1 23 } 24 25 func getPossiblyNaNOrInfFloat64(rate float64) float64 { 26 var x float64 27 if rand.Float64() < rate { 28 r := rand.Intn(3) 29 if r == 0 { 30 x = math.NaN() 31 } else { 32 // -inf if r == 1, +inf if r == 2. 33 x = math.Inf(r - 2) 34 } 35 } else { 36 // Exponentially-distributed random number in 37 // [-math.MaxFloat64, math.MaxFloat64]. 38 x = rand.ExpFloat64() 39 if rand.Intn(2) != 0 { 40 x = -x 41 } 42 } 43 return x 44 } 45 46 func TestFindNaNOrInf(t *testing.T) { 47 // Exhausively test all first-NaN/inf positions for sizes in 0..32. 48 for size := 0; size <= 32; size++ { 49 slice := make([]float64, size) 50 got := simd.FindNaNOrInf64(slice) 51 want := findNaNOrInf64Standard(slice) 52 expect.EQ(t, got, want) 53 expect.EQ(t, got, -1) 54 55 for target := size - 1; target >= 0; target-- { 56 slice[target] = math.Inf(1) 57 // Randomize everything after this position, maximizing entropy. 58 for i := target + 1; i < size; i++ { 59 slice[i] = getPossiblyNaNOrInfFloat64(0.5) 60 } 61 got = simd.FindNaNOrInf64(slice) 62 want = findNaNOrInf64Standard(slice) 63 expect.EQ(t, got, want) 64 expect.EQ(t, got, target) 65 } 66 for i := range slice { 67 slice[i] = 0.0 68 } 69 for target := size - 1; target >= 0; target-- { 70 slice[target] = math.NaN() 71 for i := target + 1; i < size; i++ { 72 slice[i] = getPossiblyNaNOrInfFloat64(0.5) 73 } 74 got = simd.FindNaNOrInf64(slice) 75 want = findNaNOrInf64Standard(slice) 76 expect.EQ(t, got, want) 77 expect.EQ(t, got, target) 78 } 79 } 80 // Random test for larger sizes. 81 maxSize := 30000 82 nIter := 200 83 rand.Seed(1) 84 for iter := 0; iter < nIter; iter++ { 85 size := 1 + rand.Intn(maxSize) 86 rate := rand.Float64() 87 slice := make([]float64, size) 88 for i := range slice { 89 slice[i] = getPossiblyNaNOrInfFloat64(rate) 90 } 91 92 for pos := 0; ; { 93 got := simd.FindNaNOrInf64(slice[pos:]) 94 want := findNaNOrInf64Standard(slice[pos:]) 95 expect.EQ(t, got, want) 96 if got == -1 { 97 break 98 } 99 pos += got + 1 100 } 101 } 102 } 103 104 type float64Args struct { 105 main []float64 106 } 107 108 func findNaNOrInfSimdSubtask(args interface{}, nIter int) int { 109 a := args.(float64Args) 110 slice := a.main 111 sum := 0 112 pos := 0 113 for iter := 0; iter < nIter; iter++ { 114 got := simd.FindNaNOrInf64(slice[pos:]) 115 sum += got 116 if got == -1 { 117 pos = 0 118 } else { 119 pos += got + 1 120 } 121 } 122 return sum 123 } 124 125 func findNaNOrInf64Bitwise(data []float64) int { 126 for i, x := range data { 127 // Extract the exponent bits, and check if they're all set: that (and only 128 // that) corresponds to NaN/inf. 129 // Interestingly, the performance of this idiom degrades significantly, 130 // relative to 131 // "math.IsNaN(x) || x > math.MaxFloat64 || x < -math.MaxFloat64", 132 // if x is interpreted as a float64 anywhere in this loop. 133 if (math.Float64bits(x) & (0x7ff << 52)) == (0x7ff << 52) { 134 return i 135 } 136 } 137 return -1 138 } 139 140 func findNaNOrInfBitwiseSubtask(args interface{}, nIter int) int { 141 a := args.(float64Args) 142 slice := a.main 143 sum := 0 144 pos := 0 145 for iter := 0; iter < nIter; iter++ { 146 got := findNaNOrInf64Bitwise(slice[pos:]) 147 sum += got 148 if got == -1 { 149 pos = 0 150 } else { 151 pos += got + 1 152 } 153 } 154 return sum 155 } 156 157 func findNaNOrInfStandardSubtask(args interface{}, nIter int) int { 158 a := args.(float64Args) 159 slice := a.main 160 sum := 0 161 pos := 0 162 for iter := 0; iter < nIter; iter++ { 163 got := findNaNOrInf64Standard(slice[pos:]) 164 sum += got 165 if got == -1 { 166 pos = 0 167 } else { 168 pos += got + 1 169 } 170 } 171 return sum 172 } 173 174 // On an m5.16xlarge: 175 // $ bazel run //go/src/github.com/grailbio/base/simd:go_default_test -- -test.bench=FindNaNOrInf 176 // ... 177 // Benchmark_FindNaNOrInf/SIMDLong1Cpu-64 82 14053127 ns/op 178 // Benchmark_FindNaNOrInf/SIMDLongHalfCpu-64 960 1143599 ns/op 179 // Benchmark_FindNaNOrInf/SIMDLongAllCpu-64 1143 1018525 ns/op 180 // Benchmark_FindNaNOrInf/BitwiseLong1Cpu-64 8 126930287 ns/op 181 // Benchmark_FindNaNOrInf/BitwiseLongHalfCpu-64 253 6668467 ns/op 182 // Benchmark_FindNaNOrInf/BitwiseLongAllCpu-64 229 4679633 ns/op 183 // Benchmark_FindNaNOrInf/StandardLong1Cpu-64 7 158318559 ns/op 184 // Benchmark_FindNaNOrInf/StandardLongHalfCpu-64 190 6223669 ns/op 185 // Benchmark_FindNaNOrInf/StandardLongAllCpu-64 171 6746008 ns/op 186 // PASS 187 func Benchmark_FindNaNOrInf(b *testing.B) { 188 funcs := []taggedMultiBenchVarargsFunc{ 189 { 190 f: findNaNOrInfSimdSubtask, 191 tag: "SIMD", 192 }, 193 { 194 f: findNaNOrInfBitwiseSubtask, 195 tag: "Bitwise", 196 }, 197 { 198 f: findNaNOrInfStandardSubtask, 199 tag: "Standard", 200 }, 201 } 202 rand.Seed(1) 203 for _, f := range funcs { 204 multiBenchmarkVarargs(f.f, f.tag+"Long", 100000, func() interface{} { 205 main := make([]float64, 30000) 206 // Results were overly influenced by RNG if the number of NaNs/infs in 207 // the slice was not controlled. 208 for i := 0; i < 30; i++ { 209 for { 210 pos := rand.Intn(len(main)) 211 if main[pos] != math.Inf(0) { 212 main[pos] = math.Inf(0) 213 break 214 } 215 } 216 } 217 return float64Args{ 218 main: main, 219 } 220 }, b) 221 } 222 }