Skip to content
This repository was archived by the owner on Jul 22, 2024. It is now read-only.

Commit 1f6d247

Browse files
authored
Improve allocation performance
* improve reflect and option allocations * optimize binary writes for numbers * optimize string writes(#3) * optimize object wrapping * update go.mod to 1.20 to support unsafe string conversion
1 parent cd1b72b commit 1f6d247

File tree

2 files changed

+139
-56
lines changed

2 files changed

+139
-56
lines changed

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module github.com/gohugoio/hashstructure
22

3-
go 1.18
3+
go 1.20
44

55
require github.com/cespare/xxhash/v2 v2.3.0

‎hashstructure.go‎

Lines changed: 138 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import (
55
"fmt"
66
"hash"
77
"hash/fnv"
8-
"io"
8+
"math"
99
"reflect"
1010
"time"
11+
"unsafe"
1112
)
1213

1314
// HashOptions are options that are available for hashing.
@@ -115,6 +116,7 @@ type walker struct {
115116
ignorezerovalue bool
116117
sets bool
117118
stringer bool
119+
buf [16]byte // Reusable buffer for binary encoding
118120
}
119121

120122
type visitOpts struct {
@@ -131,8 +133,54 @@ var timeType = reflect.TypeOf(time.Time{})
131133
// A direct hash calculation used for numeric and bool values.
132134
func (w *walker) hashDirect(v any) (uint64, error) {
133135
w.h.Reset()
134-
err := binary.Write(w.h, binary.LittleEndian, v)
135-
return w.h.Sum64(), err
136+
137+
// Use direct byte manipulation for numbers instead of binary.Write to avoid allocations
138+
switch val := v.(type) {
139+
case int64:
140+
binary.LittleEndian.PutUint64(w.buf[:8], uint64(val))
141+
w.h.Write(w.buf[:8])
142+
case uint64:
143+
binary.LittleEndian.PutUint64(w.buf[:8], val)
144+
w.h.Write(w.buf[:8])
145+
case int8:
146+
w.buf[0] = byte(val)
147+
w.h.Write(w.buf[:1])
148+
case uint8:
149+
w.buf[0] = val
150+
w.h.Write(w.buf[:1])
151+
case int16:
152+
binary.LittleEndian.PutUint16(w.buf[:2], uint16(val))
153+
w.h.Write(w.buf[:2])
154+
case uint16:
155+
binary.LittleEndian.PutUint16(w.buf[:2], val)
156+
w.h.Write(w.buf[:2])
157+
case int32:
158+
binary.LittleEndian.PutUint32(w.buf[:4], uint32(val))
159+
w.h.Write(w.buf[:4])
160+
case uint32:
161+
binary.LittleEndian.PutUint32(w.buf[:4], val)
162+
w.h.Write(w.buf[:4])
163+
case float32:
164+
binary.LittleEndian.PutUint32(w.buf[:4], math.Float32bits(val))
165+
w.h.Write(w.buf[:4])
166+
case float64:
167+
binary.LittleEndian.PutUint64(w.buf[:8], math.Float64bits(val))
168+
w.h.Write(w.buf[:8])
169+
case complex64:
170+
binary.LittleEndian.PutUint32(w.buf[:4], math.Float32bits(real(val)))
171+
binary.LittleEndian.PutUint32(w.buf[4:8], math.Float32bits(imag(val)))
172+
w.h.Write(w.buf[:8])
173+
case complex128:
174+
binary.LittleEndian.PutUint64(w.buf[:8], math.Float64bits(real(val)))
175+
binary.LittleEndian.PutUint64(w.buf[8:16], math.Float64bits(imag(val)))
176+
w.h.Write(w.buf[:16])
177+
default:
178+
// Fallback to binary.Write for unsupported types, for instance enums
179+
err := binary.Write(w.h, binary.LittleEndian, v)
180+
return w.h.Sum64(), err
181+
}
182+
183+
return w.h.Sum64(), nil
136184
}
137185

138186
// A direct hash calculation used for strings.
@@ -144,10 +192,12 @@ func (w *walker) hashString(s string) (uint64, error) {
144192
func hashString(h hash.Hash64, s string) (uint64, error) {
145193
h.Reset()
146194

147-
// io.WriteString uses io.StringWriter if it exists, which is
148-
// implemented by e.g. github.com/cespare/xxhash.
149-
_, err := io.WriteString(h, s)
150-
return h.Sum64(), err
195+
// Use zero-copy conversion from string to []byte using unsafe
196+
if len(s) > 0 {
197+
b := unsafe.Slice(unsafe.StringData(s), len(s))
198+
h.Write(b)
199+
}
200+
return h.Sum64(), nil
151201
}
152202

153203
func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
@@ -181,23 +231,55 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
181231
}
182232

183233
if v.CanInt() {
184-
if v.Kind() == reflect.Int {
185-
// binary.Write requires a fixed-size value.
186-
return w.hashDirect(v.Int())
234+
i := v.Int()
235+
switch v.Kind() {
236+
case reflect.Int:
237+
return w.hashDirect(i)
238+
case reflect.Int8:
239+
return w.hashDirect(int8(i))
240+
case reflect.Int16:
241+
return w.hashDirect(int16(i))
242+
case reflect.Int32:
243+
return w.hashDirect(int32(i))
244+
case reflect.Int64:
245+
return w.hashDirect(i)
187246
}
188-
return w.hashDirect(v.Interface())
189247
}
190248

191249
if v.CanUint() {
192-
if v.Kind() == reflect.Uint {
193-
// binary.Write requires a fixed-size value.
194-
return w.hashDirect(v.Uint())
250+
u := v.Uint()
251+
switch v.Kind() {
252+
case reflect.Uint:
253+
return w.hashDirect(u)
254+
case reflect.Uint8:
255+
return w.hashDirect(uint8(u))
256+
case reflect.Uint16:
257+
return w.hashDirect(uint16(u))
258+
case reflect.Uint32:
259+
return w.hashDirect(uint32(u))
260+
case reflect.Uint64:
261+
return w.hashDirect(u)
195262
}
196-
return w.hashDirect(v.Interface())
197263
}
198264

199-
if v.CanFloat() || v.CanComplex() {
200-
return w.hashDirect(v.Interface())
265+
if v.CanFloat() {
266+
f := v.Float()
267+
switch v.Kind() {
268+
case reflect.Float32:
269+
return w.hashDirect(float32(f))
270+
case reflect.Float64:
271+
return w.hashDirect(f)
272+
}
273+
}
274+
275+
if v.CanComplex() {
276+
c := v.Complex()
277+
switch v.Kind() {
278+
case reflect.Complex64:
279+
return w.hashDirect(complex64(c))
280+
case reflect.Complex128:
281+
return w.hashDirect(c)
282+
}
201283
}
202284

203285
k := v.Kind()
@@ -218,8 +300,8 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
218300
return 0, err
219301
}
220302

221-
err = binary.Write(w.h, binary.LittleEndian, b)
222-
return w.h.Sum64(), err
303+
w.h.Write(b)
304+
return w.h.Sum64(), nil
223305
}
224306

225307
switch k {
@@ -290,18 +372,10 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
290372
return h, nil
291373

292374
case reflect.Struct:
293-
parent := v.Interface()
294375
var include Includable
295-
if impl, ok := parent.(Includable); ok {
296-
include = impl
297-
}
298-
299-
if impl, ok := parent.(Hashable); ok {
300-
return impl.Hash()
301-
}
376+
var parent interface{}
302377

303-
// If we can address this value, check if the pointer value
304-
// implements our interfaces and use that if so.
378+
// Check if we can address this value first (more common case for pointer receivers)
305379
if v.CanAddr() {
306380
vptr := v.Addr()
307381
parentptr := vptr.Interface()
@@ -312,18 +386,38 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
312386
if impl, ok := parentptr.(Hashable); ok {
313387
return impl.Hash()
314388
}
389+
// Only set parent if we'll need it for IncludableMap
390+
parent = parentptr
391+
}
392+
393+
// Only box the value if we haven't already found an implementation via pointer
394+
if include == nil && parent == nil {
395+
parent = v.Interface()
396+
if impl, ok := parent.(Includable); ok {
397+
include = impl
398+
}
399+
400+
if impl, ok := parent.(Hashable); ok {
401+
return impl.Hash()
402+
}
315403
}
316404

317405
t := v.Type()
318-
h, err := w.visit(reflect.ValueOf(t.Name()), nil)
406+
h, err := w.hashString(t.Name())
319407
if err != nil {
320408
return 0, err
321409
}
322410

323411
l := v.NumField()
412+
var fieldOpts visitOpts
413+
// Defer boxing parent until we know we need it
414+
if parent == nil {
415+
parent = v.Interface()
416+
}
417+
fieldOpts.Struct = parent
418+
324419
for i := 0; i < l; i++ {
325420
if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
326-
var f visitFlag
327421
fieldType := t.Field(i)
328422
if fieldType.PkgPath != "" {
329423
// Unexported
@@ -366,21 +460,18 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
366460
}
367461
}
368462

369-
switch tag {
370-
case "set":
371-
f |= visitFlagSet
463+
fieldOpts.Flags = 0
464+
if tag == "set" {
465+
fieldOpts.Flags |= visitFlagSet
372466
}
373467

374-
kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil)
468+
kh, err := w.hashString(fieldType.Name)
375469
if err != nil {
376470
return 0, err
377471
}
378472

379-
vh, err := w.visit(innerV, &visitOpts{
380-
Flags: f,
381-
Struct: parent,
382-
StructField: fieldType.Name,
383-
})
473+
fieldOpts.StructField = fieldType.Name
474+
vh, err := w.visit(innerV, &fieldOpts)
384475
if err != nil {
385476
return 0, err
386477
}
@@ -435,16 +526,10 @@ func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 {
435526
// For ordered updates, use a real hash function
436527
h.Reset()
437528

438-
// We just panic if the binary writes fail because we are writing
439-
// an int64 which should never be fail-able.
440-
e1 := binary.Write(h, binary.LittleEndian, a)
441-
e2 := binary.Write(h, binary.LittleEndian, b)
442-
if e1 != nil {
443-
panic(e1)
444-
}
445-
if e2 != nil {
446-
panic(e2)
447-
}
529+
var buf [16]byte
530+
binary.LittleEndian.PutUint64(buf[0:8], a)
531+
binary.LittleEndian.PutUint64(buf[8:16], b)
532+
h.Write(buf[:])
448533

449534
return h.Sum64()
450535
}
@@ -470,11 +555,9 @@ func hashUpdateUnordered(a, b uint64) uint64 {
470555
func hashFinishUnordered(h hash.Hash64, a uint64) uint64 {
471556
h.Reset()
472557

473-
// We just panic if the writes fail
474-
e1 := binary.Write(h, binary.LittleEndian, a)
475-
if e1 != nil {
476-
panic(e1)
477-
}
558+
var buf [8]byte
559+
binary.LittleEndian.PutUint64(buf[:], a)
560+
h.Write(buf[:])
478561

479562
return h.Sum64()
480563
}

0 commit comments

Comments
 (0)