Skip to content

Commit 909eabc

Browse files
committed
Some performance optimizations
* Avoid creating new `reflect.Value`s for common int, uint and bool types. * Use `io.WriteString` to write strings. This uses `io.StringWriter` if it exists, which is implemented by e.g. github.com/cespare/xxhash. Compared to master: ``` goos: darwin goarch: arm64 pkg: github.com/gohugoio/hashstructure cpu: Apple M1 Pro │ cmpmaster.bench │ perf-20250205.bench │ │ sec/op │ sec/op vs base │ Map-10 1.963µ ± 17% 1.291µ ± 13% -34.23% (p=0.002 n=6) String/default-10 80.90n ± 1% 84.41n ± 0% +4.34% (p=0.002 n=6) String/xxhash-10 54.83n ± 1% 40.12n ± 0% -26.82% (p=0.002 n=6) geomean 205.7n 163.5n -20.52% │ cmpmaster.bench │ perf-20250205.bench │ │ B/op │ B/op vs base │ Map-10 573.5 ± 22% 382.0 ± 13% -33.39% (p=0.002 n=6) String/default-10 56.00 ± 0% 56.00 ± 0% ~ (p=1.000 n=6) ¹ String/xxhash-10 48.00 ± 0% 16.00 ± 0% -66.67% (p=0.002 n=6) geomean 115.5 69.95 -39.45% ¹ all samples are equal │ cmpmaster.bench │ perf-20250205.bench │ │ allocs/op │ allocs/op vs base │ Map-10 57.50 ± 20% 37.00 ± 14% -35.65% (p=0.002 n=6) String/default-10 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ String/xxhash-10 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.002 n=6) geomean 7.014 4.806 -31.48% ``` Compared to `mitchellh/hashstructure`: ``` goos: darwin goarch: arm64 pkg: github.com/gohugoio/hashstructure cpu: Apple M1 Pro │ cmpfork.bench │ perf-20250205.bench │ │ sec/op │ sec/op vs base │ Map-10 2.789µ ± 6% 1.292µ ± 41% -53.69% (p=0.002 n=6) String/default-10 83.45n ± 0% 87.36n ± 1% +4.69% (p=0.002 n=6) String/xxhash-10 56.19n ± 0% 41.59n ± 1% -25.98% (p=0.002 n=6) geomean 235.6n 167.4n -28.94% │ cmpfork.bench │ perf-20250205.bench │ │ B/op │ B/op vs base │ Map-10 1461.0 ± 6% 393.0 ± 27% -73.10% (p=0.002 n=6) String/default-10 56.00 ± 0% 56.00 ± 0% ~ (p=1.000 n=6) ¹ String/xxhash-10 48.00 ± 0% 16.00 ± 0% -66.67% (p=0.002 n=6) geomean 157.8 70.62 -55.24% ¹ all samples are equal │ cmpfork.bench │ perf-20250205.bench │ │ allocs/op │ allocs/op vs base │ Map-10 87.50 ± 9% 36.50 ± 40% -58.29% (p=0.002 n=6) String/default-10 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=6) ¹ String/xxhash-10 2.000 ± 0% 1.000 ± 0% -50.00% (p=0.002 n=6) geomean 8.067 4.784 -40.70% ````
1 parent 57e8e3b commit 909eabc

File tree

4 files changed

+58
-19
lines changed

4 files changed

+58
-19
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/gohugoio/hashstructure
22

33
go 1.18
4+
5+
require github.com/cespare/xxhash/v2 v2.3.0

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
2+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=

hashstructure.go

+35-19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"hash"
77
"hash/fnv"
8+
"io"
89
"reflect"
910
"time"
1011
)
@@ -122,6 +123,13 @@ type visitOpts struct {
122123

123124
var timeType = reflect.TypeOf(time.Time{})
124125

126+
// A direct hash calculation used for numeric and bool values.
127+
func (w *walker) hashDirect(v any) (uint64, error) {
128+
w.h.Reset()
129+
err := binary.Write(w.h, binary.LittleEndian, v)
130+
return w.h.Sum64(), err
131+
}
132+
125133
func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
126134
t := reflect.TypeOf(0)
127135

@@ -152,29 +160,34 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
152160
v = reflect.Zero(t)
153161
}
154162

155-
// Binary writing can use raw ints, we have to convert to
156-
// a sized-int, we'll choose the largest...
157-
switch v.Kind() {
158-
case reflect.Int:
159-
v = reflect.ValueOf(int64(v.Int()))
160-
case reflect.Uint:
161-
v = reflect.ValueOf(uint64(v.Uint()))
162-
case reflect.Bool:
163-
var tmp int8
164-
if v.Bool() {
165-
tmp = 1
163+
if v.CanInt() {
164+
if v.Kind() == reflect.Int {
165+
// binary.Write requires a fixed-size value.
166+
return w.hashDirect(v.Int())
166167
}
167-
v = reflect.ValueOf(tmp)
168+
return w.hashDirect(v.Interface())
169+
}
170+
171+
if v.CanUint() {
172+
if v.Kind() == reflect.Uint {
173+
// binary.Write requires a fixed-size value.
174+
return w.hashDirect(v.Uint())
175+
}
176+
return w.hashDirect(v.Interface())
177+
}
178+
179+
if v.CanFloat() {
180+
return w.hashDirect(v.Interface())
168181
}
169182

170183
k := v.Kind()
171184

172-
// We can shortcut numeric values by directly binary writing them
173-
if k >= reflect.Int && k <= reflect.Complex64 {
174-
// A direct hash calculation
175-
w.h.Reset()
176-
err := binary.Write(w.h, binary.LittleEndian, v.Interface())
177-
return w.h.Sum64(), err
185+
if k == reflect.Bool {
186+
var tmp int8
187+
if v.Bool() {
188+
tmp = 1
189+
}
190+
return w.hashDirect(tmp)
178191
}
179192

180193
switch v.Type() {
@@ -394,7 +407,10 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
394407
case reflect.String:
395408
// Directly hash
396409
w.h.Reset()
397-
_, err := w.h.Write([]byte(v.String()))
410+
411+
// io.WriteString uses io.StringWriter if it exists, which is
412+
// implemented by e.g. github.com/cespare/xxhash.
413+
_, err := io.WriteString(w.h, v.String())
398414
return w.h.Sum64(), err
399415

400416
default:

hashstructure_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"strings"
66
"testing"
77
"time"
8+
9+
"github.com/cespare/xxhash/v2"
810
)
911

1012
func TestHash_identity(t *testing.T) {
@@ -727,6 +729,7 @@ func TestHash_golden(t *testing.T) {
727729
In: int64(42),
728730
Expect: 11375694726533372055,
729731
},
732+
730733
{
731734
In: uint16(42),
732735
Expect: 590708257076254031,
@@ -846,6 +849,22 @@ func BenchmarkMap(b *testing.B) {
846849
}
847850
}
848851

852+
func BenchmarkString(b *testing.B) {
853+
s := "lorem ipsum dolor sit amet"
854+
b.Run("default", func(b *testing.B) {
855+
for i := 0; i < b.N; i++ {
856+
Hash(s, nil)
857+
}
858+
})
859+
860+
b.Run("xxhash", func(b *testing.B) {
861+
opts := &HashOptions{Hasher: xxhash.New()}
862+
for i := 0; i < b.N; i++ {
863+
Hash(s, opts)
864+
}
865+
})
866+
}
867+
849868
type testIncludable struct {
850869
Value string
851870
Ignore string

0 commit comments

Comments
 (0)