From e776e0e20cbda5c0c0144007b707ef281a347315 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 4 Oct 2023 00:35:25 +0100 Subject: [PATCH 1/3] cmp: optimize Compare Compare provides three-way comparison for Ordered types, which certain algorithms benefit from. Float comparisons require NaN handling that should be optimized away for all other types. String comparisons benefit from trying == first and short-circuiting on different length strings. Ideally the compiler would recognize the pattern and specialize on strings, but this improves the status quo. Benchmark shows: - no change for ints, - 20% improvement for floats, and - 25% improvement for strings. Bug: #61725 --- src/cmp/bench_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ src/cmp/cmp.go | 15 ++++++++------- 2 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/cmp/bench_test.go diff --git a/src/cmp/bench_test.go b/src/cmp/bench_test.go new file mode 100644 index 00000000000000..4323369deaa70b --- /dev/null +++ b/src/cmp/bench_test.go @@ -0,0 +1,44 @@ +package cmp_test + +import ( + "cmp" + "slices" + "strconv" + "testing" +) + +func BenchmarkCompare_int(b *testing.B) { + var lst [1_000_000]int + for i := range lst { + lst[i] = i + } + b.ResetTimer() + + for n := 0; n < b.N; n++ { + slices.SortFunc(lst[:], cmp.Compare) + } +} + +func BenchmarkCompare_float64(b *testing.B) { + var lst [1_000_000]float64 + for i := range lst { + lst[i] = float64(i) + } + b.ResetTimer() + + for n := 0; n < b.N; n++ { + slices.SortFunc(lst[:], cmp.Compare) + } +} + +func BenchmarkCompare_string(b *testing.B) { + var lst [1_000_000]string + for i := range lst { + lst[i] = strconv.Itoa(i) + } + b.ResetTimer() + + for n := 0; n < b.N; n++ { + slices.SortFunc(lst[:], cmp.Compare) + } +} diff --git a/src/cmp/cmp.go b/src/cmp/cmp.go index 4d1af6a98c4e46..87a154e87b21af 100644 --- a/src/cmp/cmp.go +++ b/src/cmp/cmp.go @@ -38,18 +38,19 @@ func Less[T Ordered](x, y T) bool { // For floating-point types, a NaN is considered less than any non-NaN, // a NaN is considered equal to a NaN, and -0.0 is equal to 0.0. func Compare[T Ordered](x, y T) int { - xNaN := isNaN(x) - yNaN := isNaN(y) - if xNaN && yNaN { + if x == y { return 0 } - if xNaN || x < y { + if x < y { return -1 } - if yNaN || x > y { - return +1 + if isNaN(x) { + if isNaN(y) { + return 0 + } + return -1 } - return 0 + return +1 } // isNaN reports whether x is a NaN without requiring the math package. From d74b2a97615383ab1b89d1a8528c3ba5177a2e7c Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Wed, 4 Oct 2023 17:29:21 +0100 Subject: [PATCH 2/3] Improve benchmark, "do no harm" optimization. --- src/cmp/bench_test.go | 55 ++++++++++++++++++++++++++----------------- src/cmp/cmp.go | 22 ++++++++--------- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/cmp/bench_test.go b/src/cmp/bench_test.go index 4323369deaa70b..d3541a672a3d42 100644 --- a/src/cmp/bench_test.go +++ b/src/cmp/bench_test.go @@ -2,43 +2,56 @@ package cmp_test import ( "cmp" - "slices" - "strconv" + "math" "testing" ) +var sum int + func BenchmarkCompare_int(b *testing.B) { - var lst [1_000_000]int - for i := range lst { - lst[i] = i + lst := [...]int{ + 0xfa3, 0x7fe, 0x03c, 0xcb9, + 0x4ce, 0x4fb, 0x7d5, 0x38f, + 0x73b, 0x322, 0x85c, 0xf4d, + 0xbbc, 0x032, 0x059, 0xb93, } - b.ResetTimer() - for n := 0; n < b.N; n++ { - slices.SortFunc(lst[:], cmp.Compare) + sum += cmp.Compare(lst[n%len(lst)], lst[(2*n)%len(lst)]) } } func BenchmarkCompare_float64(b *testing.B) { - var lst [1_000_000]float64 - for i := range lst { - lst[i] = float64(i) + lst := [...]float64{ + 0.35573281, 0.77552566, 0.19006500, 0.66436280, + 0.02769279, 0.97572397, 0.40945068, 0.26422857, + 0.10985792, 0.35659522, 0.82752613, 0.18875522, + 0.16410543, 0.03578153, 0.51636871, math.NaN(), } - b.ResetTimer() - for n := 0; n < b.N; n++ { - slices.SortFunc(lst[:], cmp.Compare) + sum += cmp.Compare(lst[n%len(lst)], lst[(2*n)%len(lst)]) } } -func BenchmarkCompare_string(b *testing.B) { - var lst [1_000_000]string - for i := range lst { - lst[i] = strconv.Itoa(i) +func BenchmarkCompare_strings(b *testing.B) { + lst := [...]string{ + "time", + "person", + "year", + "way", + "day", + "thing", + "man", + "world", + "life", + "hand", + "part", + "child", + "eye", + "woman", + "place", + "work", } - b.ResetTimer() - for n := 0; n < b.N; n++ { - slices.SortFunc(lst[:], cmp.Compare) + sum += cmp.Compare(lst[n%len(lst)], lst[(2*n)%len(lst)]) } } diff --git a/src/cmp/cmp.go b/src/cmp/cmp.go index 87a154e87b21af..491e43c124968c 100644 --- a/src/cmp/cmp.go +++ b/src/cmp/cmp.go @@ -38,19 +38,19 @@ func Less[T Ordered](x, y T) bool { // For floating-point types, a NaN is considered less than any non-NaN, // a NaN is considered equal to a NaN, and -0.0 is equal to 0.0. func Compare[T Ordered](x, y T) int { - if x == y { - return 0 - } - if x < y { - return -1 - } - if isNaN(x) { - if isNaN(y) { - return 0 + if x != y { + if isNaN(x) { + if isNaN(y) { + return 0 + } + return -1 + } + if x < y { + return -1 } - return -1 + return +1 } - return +1 + return 0 } // isNaN reports whether x is a NaN without requiring the math package. From b3bfbdf1d0722a2ff7d5ea3dde1aeeb2c96170e3 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 9 Oct 2023 11:41:50 +0100 Subject: [PATCH 3/3] Add NaN comment. --- src/cmp/cmp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cmp/cmp.go b/src/cmp/cmp.go index 491e43c124968c..fc69b8b2294a73 100644 --- a/src/cmp/cmp.go +++ b/src/cmp/cmp.go @@ -45,6 +45,7 @@ func Compare[T Ordered](x, y T) int { } return -1 } + // If isNaN(y), x < y is false. if x < y { return -1 }