Skip to content

Commit

Permalink
Better NaN handling for fastMin and fastMax
Browse files Browse the repository at this point in the history
The `fastMin` and `fastMax` functions previously didn't taken NaN into
account, so returned inaccurate results when NaNs were passed in,
biasing NaN to be returned only if it were the first argument.

This change makes the behaviour of the functions symmetrical, returning
NaN if _either_ argument (or both) are NaN.

Benchmarks have been added to show that indeed the fast variants are
significantly faster than their standard variants.

```
$ go test -run=XXX -bench='Fast|Math' ./geom
goos: linux
goarch: arm64
pkg: github.com/peterstace/simplefeatures/geom
BenchmarkFastMin-2      712878825                1.574 ns/op
BenchmarkFastMax-2      1000000000               1.101 ns/op
BenchmarkMathMin-2      149962322                7.861 ns/op
BenchmarkMathMax-2      152543700                7.866 ns/op
PASS
```

The motivation behind this change is to remove the error from Envelope
constructors, and perform validation separately as its own method
(similar to the recent changes from geometries). Since the envelope only
stores the min and max points, I'd like any NaNs to "infect" the
envelope so that they're caught by validation rather than silently being
dropped.
  • Loading branch information
peterstace committed Oct 5, 2023
1 parent 80be3df commit 08b78b9
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 2 deletions.
5 changes: 3 additions & 2 deletions geom/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package geom

import (
"fmt"
"math"
"sort"
)

Expand Down Expand Up @@ -69,15 +70,15 @@ func sequenceToXYs(seq Sequence) []XY {

// fastMin is a faster but not functionally identical version of math.Min.
func fastMin(a, b float64) float64 {
if a < b {
if math.IsNaN(a) || a < b {
return a
}
return b
}

// fastMax is a faster but not functionally identical version of math.Max.
func fastMax(a, b float64) float64 {
if a > b {
if math.IsNaN(a) || a > b {
return a
}
return b
Expand Down
71 changes: 71 additions & 0 deletions geom/util_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package geom

import (
"math"
"strconv"
"testing"
)

func TestFastMinFastMax(t *testing.T) {
var (
nan = math.NaN()
inf = math.Inf(1)
eq = func(a, b float64) bool {
return (math.IsNaN(a) && math.IsNaN(b)) || a == b
}
)
for i, tc := range []struct {
a, b float64
min, max float64
}{
{0, 0, 0, 0},
{1, 2, 1, 2},
{2, 1, 1, 2},
{0, nan, nan, nan},
{nan, 0, nan, nan},
{nan, nan, nan, nan},
{0, inf, 0, inf},
{inf, 0, 0, inf},
{inf, inf, inf, inf},
{0, -inf, -inf, 0},
{-inf, 0, -inf, 0},
{-inf, -inf, -inf, -inf},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
gotMin := fastMin(tc.a, tc.b)
gotMax := fastMax(tc.a, tc.b)
if !eq(gotMin, tc.min) {
t.Errorf("fastMin(%v, %v) = %v, want %v", tc.a, tc.b, gotMin, tc.min)
}
if !eq(gotMax, tc.max) {
t.Errorf("fastMax(%v, %v) = %v, want %v", tc.a, tc.b, gotMax, tc.max)
}
})
}
}

var global float64

func BenchmarkFastMin(b *testing.B) {
for i := 0; i < b.N; i++ {
global = fastMin(global, 2)
}
}

func BenchmarkFastMax(b *testing.B) {
for i := 0; i < b.N; i++ {
global = fastMax(global, 2)
}
}

func BenchmarkMathMin(b *testing.B) {
for i := 0; i < b.N; i++ {
global = math.Min(global, 2)
}
}

func BenchmarkMathMax(b *testing.B) {
for i := 0; i < b.N; i++ {
global = math.Max(global, 2)
}
}

0 comments on commit 08b78b9

Please sign in to comment.