vector: clarify GOARCH=wasm test code
Change-Id: If190b889e4a6db44416615bbda487a677e8e9044
Reviewed-on: https://go-review.googlesource.com/c/image/+/172517
Reviewed-by: Richard Musiol <neelance@gmail.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/vector/acc_test.go b/vector/acc_test.go
index bfac42c..16815cf 100644
--- a/vector/acc_test.go
+++ b/vector/acc_test.go
@@ -245,31 +245,72 @@
}
}
-func match(got, want float64) bool {
- if runtime.GOARCH == "wasm" {
- // On wasm, calculations on float32 values are done with 64 bit precision.
- // This is allowed by the Go specification, only explicit conversions to
- // float32 have to round to 32 bit precision. The difference caused by the
- // additional precision accumulates over several calculations and causes
- // the test results to not fully match the expectations. Account for this
- // by giving a 0.1% tolerance.
- const tolerance = 0.001
- return math.Abs(got-want) <= math.Abs(want)*tolerance
- }
- return got == want
+// This package contains multiple implementations of the same algorithm, e.g.
+// there are both SIMD and non-SIMD (vanilla) implementations on GOARCH=amd64.
+// In general, the tests in this file check that the output is *exactly* the
+// same, regardless of implementation.
+//
+// On GOARCH=wasm, float32 arithmetic is done with 64 bit precision. This is
+// allowed by the Go specification: only explicit conversions to float32 have
+// to round to 32 bit precision. However, the vanilla implementation therefore
+// produces different output for GOARCH=wasm than on other GOARCHes.
+//
+// We therefore treat GOARCH=wasm as a special case, where the tests check that
+// the output is only *approximately* the same (within a 0.1% tolerance).
+//
+// It's not that, on GOARCH=wasm, we produce the "wrong" answer. In fact, the
+// computation is more, not less, accurate on GOARCH=wasm. It's that the golden
+// output that the tests compare to were, for historical reasons, produced on
+// GOARCH=amd64 and so done with less accuracy (where float32 arithmetic is
+// performed entirely with 32 bits, not with 64 bits and then rounded back to
+// 32 bits). Furthermore, on amd64, we still want to test that SIMD and
+// non-SIMD produce exactly the same (albeit less accurate) output. The SIMD
+// implementation in particular is limited by what the underlying hardware
+// instructions provide, which often favors speed over accuracy.
+
+// approxEquals returns whether got is within 0.1% of want.
+func approxEquals(got, want float64) bool {
+ const tolerance = 0.001
+ return math.Abs(got-want) <= math.Abs(want)*tolerance
}
-// sixteen is used by the calculation in the test below. It needs to be a package
-// variable so the compiler does not replace the calculation with a single constant.
+// sixteen is used by TestFloat32ArithmeticWithinTolerance, below. It needs to
+// be a package-level variable so that the compiler does not replace the
+// calculation with a single constant.
var sixteen float32 = 16
-// TestFloat32ArithmeticWithinTolerance checks that the precision difference of a
-// single float32 operation is within the tolerance used by the match function.
+// TestFloat32ArithmeticWithinTolerance checks that approxEquals' tolerance is
+// sufficiently high so that the results of two separate ways of computing the
+// arbitrary fraction 16 / 1122 are deemed "approximately equal" even if they
+// aren't "exactly equal".
+//
+// We're not testing whether the computation on amd64 or wasm is "right" or
+// "wrong". We're testing that we cope with them being different.
+//
+// On GOARCH=amd64, printing x and y gives:
+// 0.0142602495543672
+// 0.014260249212384224
+//
+// On GOARCH=wasm, printing x and y gives:
+// 0.0142602495543672
+// 0.0142602495543672
+//
+// The infinitely precise (mathematical) answer is:
+// 0.014260249554367201426024955436720142602495543672recurring...
+// See https://play.golang.org/p/RxzKSdD_suE
+//
+// This test establishes a lower bound on approxEquals' tolerance constant.
+// Passing this one test (on all of the various supported GOARCH's) is a
+// necessary but not a sufficient condition on that value. Other tests in this
+// package that call uint32sMatch or float32sMatch (such as TestMakeFxInXxx,
+// TestMakeFlInXxx or anything calling testAcc) also require a sufficiently
+// large tolerance. But those tests are more complicated, and if there is a
+// problem with the tolerance constant, debugging this test can be simpler.
func TestFloat32ArithmeticWithinTolerance(t *testing.T) {
- result := float64(sixteen / 1122)
- rounded := float64(float32(result))
- if !match(result, rounded) {
- t.Error("result and rounded result did not match")
+ x := float64(sixteen) / 1122 // Always use 64-bit division.
+ y := float64(sixteen / 1122) // Use 32- or 64-bit division (GOARCH dependent).
+ if !approxEquals(x, y) {
+ t.Errorf("x and y were not approximately equal:\nx = %v\ny = %v", x, y)
}
}
@@ -277,9 +318,17 @@
if len(xs) != len(ys) {
return false
}
- for i := range xs {
- if !match(float64(xs[i]), float64(ys[i])) {
- return false
+ if runtime.GOARCH == "wasm" {
+ for i := range xs {
+ if !approxEquals(float64(xs[i]), float64(ys[i])) {
+ return false
+ }
+ }
+ } else {
+ for i := range xs {
+ if xs[i] != ys[i] {
+ return false
+ }
}
}
return true
@@ -289,9 +338,17 @@
if len(xs) != len(ys) {
return false
}
- for i := range xs {
- if !match(float64(xs[i]), float64(ys[i])) {
- return false
+ if runtime.GOARCH == "wasm" {
+ for i := range xs {
+ if !approxEquals(float64(xs[i]), float64(ys[i])) {
+ return false
+ }
+ }
+ } else {
+ for i := range xs {
+ if xs[i] != ys[i] {
+ return false
+ }
}
}
return true