Skip to content

Commit fd999fd

Browse files
yuryugopherbot
authored andcommitted
strings: intrinsify and optimize Compare
slices.SortFunc requires a three-way comparison and we need an efficient strings.Compare to perform three-way string comparisons. This new implementation adds bytealg.CompareString as a wrapper of runtime_cmpstring and changes Compare to use bytealg.CompareString. The new implementation of Compare with runtime_cmpstring is about 28% faster than the previous one. Fixes #61725 │ /tmp/gobench-sort-cmp.txt │ /tmp/gobench-sort-strings.txt │ │ sec/op │ sec/op vs base │ SortFuncStruct/Size16-48 918.8n ± 1% 726.6n ± 0% -20.92% (p=0.000 n=10) SortFuncStruct/Size32-48 2.666µ ± 1% 2.003µ ± 1% -24.85% (p=0.000 n=10) SortFuncStruct/Size64-48 1.934µ ± 1% 1.331µ ± 1% -31.22% (p=0.000 n=10) SortFuncStruct/Size128-48 3.560µ ± 1% 2.423µ ± 0% -31.94% (p=0.000 n=10) SortFuncStruct/Size512-48 13.019µ ± 0% 9.071µ ± 0% -30.33% (p=0.000 n=10) SortFuncStruct/Size1024-48 25.61µ ± 0% 17.75µ ± 0% -30.70% (p=0.000 n=10) geomean 4.217µ 3.018µ -28.44% Change-Id: I2513b6f8c1b9b273ef2d23f0a86f691e2d097eb6 Reviewed-on: https://go-review.googlesource.com/c/go/+/532195 Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: qiu laidongfeng2 <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 99b65ae commit fd999fd

File tree

6 files changed

+54
-26
lines changed

6 files changed

+54
-26
lines changed

src/cmp/cmp_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"math"
1111
"slices"
1212
"sort"
13+
"strings"
1314
"testing"
1415
"unsafe"
1516
)
@@ -158,8 +159,8 @@ func ExampleOr_sort() {
158159
// Sort by customer first, product second, and last by higher price
159160
slices.SortFunc(orders, func(a, b Order) int {
160161
return cmp.Or(
161-
cmp.Compare(a.Customer, b.Customer),
162-
cmp.Compare(a.Product, b.Product),
162+
strings.Compare(a.Customer, b.Customer),
163+
strings.Compare(a.Product, b.Product),
163164
cmp.Compare(b.Price, a.Price),
164165
)
165166
})

src/internal/bytealg/compare_generic.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ samebytes:
3535
return 0
3636
}
3737

38+
func CompareString(a, b string) int {
39+
return runtime_cmpstring(a, b)
40+
}
41+
3842
//go:linkname runtime_cmpstring runtime.cmpstring
3943
func runtime_cmpstring(a, b string) int {
4044
l := len(a)

src/internal/bytealg/compare_native.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import _ "unsafe" // For go:linkname
1111
//go:noescape
1212
func Compare(a, b []byte) int
1313

14+
func CompareString(a, b string) int {
15+
return abigen_runtime_cmpstring(a, b)
16+
}
17+
1418
// The declaration below generates ABI wrappers for functions
1519
// implemented in assembly in this package but declared in another
1620
// package.

src/slices/example_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func ExampleBinarySearchFunc() {
3434
{"Gopher", 13},
3535
}
3636
n, found := slices.BinarySearchFunc(people, Person{"Bob", 0}, func(a, b Person) int {
37-
return cmp.Compare(a.Name, b.Name)
37+
return strings.Compare(a.Name, b.Name)
3838
})
3939
fmt.Println("Bob:", n, found)
4040
// Output:
@@ -181,7 +181,7 @@ func ExampleIsSorted() {
181181
func ExampleIsSortedFunc() {
182182
names := []string{"alice", "Bob", "VERA"}
183183
isSortedInsensitive := slices.IsSortedFunc(names, func(a, b string) int {
184-
return cmp.Compare(strings.ToLower(a), strings.ToLower(b))
184+
return strings.Compare(strings.ToLower(a), strings.ToLower(b))
185185
})
186186
fmt.Println(isSortedInsensitive)
187187
fmt.Println(slices.IsSorted(names))
@@ -269,7 +269,7 @@ func ExampleSort() {
269269
func ExampleSortFunc_caseInsensitive() {
270270
names := []string{"Bob", "alice", "VERA"}
271271
slices.SortFunc(names, func(a, b string) int {
272-
return cmp.Compare(strings.ToLower(a), strings.ToLower(b))
272+
return strings.Compare(strings.ToLower(a), strings.ToLower(b))
273273
})
274274
fmt.Println(names)
275275
// Output:
@@ -288,7 +288,7 @@ func ExampleSortFunc_multiField() {
288288
{"Alice", 20},
289289
}
290290
slices.SortFunc(people, func(a, b Person) int {
291-
if n := cmp.Compare(a.Name, b.Name); n != 0 {
291+
if n := strings.Compare(a.Name, b.Name); n != 0 {
292292
return n
293293
}
294294
// If names are equal, order by age
@@ -312,7 +312,7 @@ func ExampleSortStableFunc() {
312312
}
313313
// Stable sort by name, keeping age ordering of Alices intact
314314
slices.SortStableFunc(people, func(a, b Person) int {
315-
return cmp.Compare(a.Name, b.Name)
315+
return strings.Compare(a.Name, b.Name)
316316
})
317317
fmt.Println(people)
318318
// Output:

src/slices/sort_benchmark_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package slices_test
66

77
import (
8+
"cmp"
89
"fmt"
910
"slices"
11+
"strings"
1012
"testing"
1113
)
1214

@@ -41,10 +43,38 @@ func BenchmarkBinarySearchFuncStruct(b *testing.B) {
4143
}
4244
midpoint := len(structs) / 2
4345
needle := &myStruct{n: (structs[midpoint].n + structs[midpoint+1].n) / 2}
44-
lessFunc := func(a, b *myStruct) int { return a.n - b.n }
46+
cmpFunc := func(a, b *myStruct) int { return a.n - b.n }
4547
b.ResetTimer()
4648
for i := 0; i < b.N; i++ {
47-
slices.BinarySearchFunc(structs, needle, lessFunc)
49+
slices.BinarySearchFunc(structs, needle, cmpFunc)
50+
}
51+
})
52+
}
53+
}
54+
55+
func BenchmarkSortFuncStruct(b *testing.B) {
56+
for _, size := range []int{16, 32, 64, 128, 512, 1024} {
57+
b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) {
58+
structs := make([]*myStruct, size)
59+
for i := range structs {
60+
structs[i] = &myStruct{
61+
a: fmt.Sprintf("string%d", i%10),
62+
n: i * 11 % size,
63+
}
64+
}
65+
cmpFunc := func(a, b *myStruct) int {
66+
if n := strings.Compare(a.a, b.a); n != 0 {
67+
return n
68+
}
69+
return cmp.Compare(a.n, b.n)
70+
}
71+
// Presort the slice so all benchmark iterations are identical.
72+
slices.SortFunc(structs, cmpFunc)
73+
b.ResetTimer()
74+
for i := 0; i < b.N; i++ {
75+
// Sort the slice twice because slices.SortFunc modifies the slice in place.
76+
slices.SortFunc(structs, func(a, b *myStruct) int { return cmpFunc(b, a) })
77+
slices.SortFunc(structs, cmpFunc)
4878
}
4979
})
5080
}

src/strings/compare.go

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,14 @@
44

55
package strings
66

7+
import "internal/bytealg"
8+
79
// Compare returns an integer comparing two strings lexicographically.
810
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b.
911
//
10-
// Compare is included only for symmetry with package bytes.
11-
// It is usually clearer and always faster to use the built-in
12-
// string comparison operators ==, <, >, and so on.
12+
// Use Compare when you need to perform a three-way comparison (with
13+
// slices.SortFunc, for example). It is usually clearer and always faster
14+
// to use the built-in string comparison operators ==, <, >, and so on.
1315
func Compare(a, b string) int {
14-
// NOTE(rsc): This function does NOT call the runtime cmpstring function,
15-
// because we do not want to provide any performance justification for
16-
// using strings.Compare. Basically no one should use strings.Compare.
17-
// As the comment above says, it is here only for symmetry with package bytes.
18-
// If performance is important, the compiler should be changed to recognize
19-
// the pattern so that all code doing three-way comparisons, not just code
20-
// using strings.Compare, can benefit.
21-
if a == b {
22-
return 0
23-
}
24-
if a < b {
25-
return -1
26-
}
27-
return +1
16+
return bytealg.CompareString(a, b)
2817
}

0 commit comments

Comments
 (0)