mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
util/deephash: optimize hashing of byte arrays, reduce allocs in Hash
name old time/op new time/op delta Hash-6 173µs ± 4% 101µs ± 3% -41.69% (p=0.000 n=10+9) HashMapAcyclic-6 101µs ± 5% 105µs ± 3% +3.52% (p=0.001 n=9+10) TailcfgNode-6 29.4µs ± 2% 16.4µs ± 3% -44.25% (p=0.000 n=8+10) name old alloc/op new alloc/op delta Hash-6 3.60kB ± 0% 1.13kB ± 0% -68.70% (p=0.000 n=10+10) HashMapAcyclic-6 2.53kB ± 0% 2.53kB ± 0% ~ (p=0.137 n=10+8) TailcfgNode-6 528B ± 0% 0B -100.00% (p=0.000 n=10+10) name old allocs/op new allocs/op delta Hash-6 84.0 ± 0% 40.0 ± 0% -52.38% (p=0.000 n=10+10) HashMapAcyclic-6 202 ± 0% 202 ± 0% ~ (all equal) TailcfgNode-6 11.0 ± 0% 0.0 -100.00% (p=0.000 n=10+10) Updates tailscale/corp#2130 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
3962744450
commit
6dc38ff25c
@ -12,6 +12,7 @@
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
"hash"
|
||||||
@ -21,12 +22,14 @@
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const scratchSize = 128
|
||||||
|
|
||||||
// hasher is reusable state for hashing a value.
|
// hasher is reusable state for hashing a value.
|
||||||
// Get one via hasherPool.
|
// Get one via hasherPool.
|
||||||
type hasher struct {
|
type hasher struct {
|
||||||
h hash.Hash
|
h hash.Hash
|
||||||
bw *bufio.Writer
|
bw *bufio.Writer
|
||||||
scratch [128]byte
|
scratch [scratchSize]byte
|
||||||
visited map[uintptr]bool
|
visited map[uintptr]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,8 +59,13 @@ func (h *hasher) Hash(v interface{}) (hash [sha256.Size]byte) {
|
|||||||
h.h.Reset()
|
h.h.Reset()
|
||||||
h.print(reflect.ValueOf(v))
|
h.print(reflect.ValueOf(v))
|
||||||
h.bw.Flush()
|
h.bw.Flush()
|
||||||
h.h.Sum(hash[:0])
|
// Sum into scratch & copy out, as hash.Hash is an interface
|
||||||
return hash
|
// so the slice necessarily escapes, and there's no sha256
|
||||||
|
// concrete type exported and we don't want the 'hash' result
|
||||||
|
// parameter to escape to the heap:
|
||||||
|
h.h.Sum(h.scratch[:0])
|
||||||
|
copy(hash[:], h.scratch[:])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasherPool = &sync.Pool{
|
var hasherPool = &sync.Pool{
|
||||||
@ -107,6 +115,16 @@ type appenderTo interface {
|
|||||||
AppendTo([]byte) []byte
|
AppendTo([]byte) []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *hasher) uint(i uint64) {
|
||||||
|
binary.BigEndian.PutUint64(h.scratch[:8], i)
|
||||||
|
h.bw.Write(h.scratch[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hasher) int(i int) {
|
||||||
|
binary.BigEndian.PutUint64(h.scratch[:8], uint64(i))
|
||||||
|
h.bw.Write(h.scratch[:8])
|
||||||
|
}
|
||||||
|
|
||||||
// print hashes v into w.
|
// print hashes v into w.
|
||||||
// It reports whether it was able to do so without hitting a cycle.
|
// It reports whether it was able to do so without hitting a cycle.
|
||||||
func (h *hasher) print(v reflect.Value) (acyclic bool) {
|
func (h *hasher) print(v reflect.Value) (acyclic bool) {
|
||||||
@ -140,31 +158,40 @@ func (h *hasher) print(v reflect.Value) (acyclic bool) {
|
|||||||
return h.print(v.Elem())
|
return h.print(v.Elem())
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
acyclic = true
|
acyclic = true
|
||||||
w.WriteString("struct{\n")
|
w.WriteString("struct")
|
||||||
|
h.int(v.NumField())
|
||||||
for i, n := 0, v.NumField(); i < n; i++ {
|
for i, n := 0, v.NumField(); i < n; i++ {
|
||||||
fmt.Fprintf(w, " [%d]: ", i)
|
h.int(i)
|
||||||
if !h.print(v.Field(i)) {
|
if !h.print(v.Field(i)) {
|
||||||
acyclic = false
|
acyclic = false
|
||||||
}
|
}
|
||||||
w.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
w.WriteString("}\n")
|
|
||||||
return acyclic
|
return acyclic
|
||||||
case reflect.Slice, reflect.Array:
|
case reflect.Slice, reflect.Array:
|
||||||
|
vLen := v.Len()
|
||||||
|
if v.Kind() == reflect.Slice {
|
||||||
|
h.int(vLen)
|
||||||
|
}
|
||||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||||
fmt.Fprintf(w, "%q", v.Interface())
|
if vLen > 0 && vLen <= scratchSize {
|
||||||
|
// If it fits in scratch, avoid the Interface allocation.
|
||||||
|
// It seems tempting to do this for all sizes, doing
|
||||||
|
// scratchSize bytes at a time, but reflect.Slice seems
|
||||||
|
// to allocate, so it's not a win.
|
||||||
|
n := reflect.Copy(reflect.ValueOf(&h.scratch).Elem(), v)
|
||||||
|
w.Write(h.scratch[:n])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s", v.Interface())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
|
||||||
acyclic = true
|
acyclic = true
|
||||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
for i := 0; i < vLen; i++ {
|
||||||
fmt.Fprintf(w, " [%d]: ", i)
|
h.int(i)
|
||||||
if !h.print(v.Index(i)) {
|
if !h.print(v.Index(i)) {
|
||||||
acyclic = false
|
acyclic = false
|
||||||
}
|
}
|
||||||
w.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
w.WriteString("}\n")
|
|
||||||
return acyclic
|
return acyclic
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
return h.print(v.Elem())
|
return h.print(v.Elem())
|
||||||
@ -196,13 +223,14 @@ func (h *hasher) print(v reflect.Value) (acyclic bool) {
|
|||||||
}
|
}
|
||||||
return h.hashMapFallback(v)
|
return h.hashMapFallback(v)
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
|
h.int(v.Len())
|
||||||
w.WriteString(v.String())
|
w.WriteString(v.String())
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
w.Write(strconv.AppendBool(h.scratch[:0], v.Bool()))
|
w.Write(strconv.AppendBool(h.scratch[:0], v.Bool()))
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
w.Write(strconv.AppendInt(h.scratch[:0], v.Int(), 10))
|
w.Write(strconv.AppendInt(h.scratch[:0], v.Int(), 10))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
w.Write(strconv.AppendUint(h.scratch[:0], v.Uint(), 10))
|
h.uint(v.Uint())
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
w.Write(strconv.AppendUint(h.scratch[:0], math.Float64bits(v.Float()), 10))
|
w.Write(strconv.AppendUint(h.scratch[:0], math.Float64bits(v.Float()), 10))
|
||||||
case reflect.Complex64, reflect.Complex128:
|
case reflect.Complex64, reflect.Complex128:
|
||||||
|
@ -169,6 +169,29 @@ func TestHashMapAcyclic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintArray(t *testing.T) {
|
||||||
|
type T struct {
|
||||||
|
X [32]byte
|
||||||
|
}
|
||||||
|
x := &T{X: [32]byte{1: 1, 31: 31}}
|
||||||
|
var got bytes.Buffer
|
||||||
|
bw := bufio.NewWriter(&got)
|
||||||
|
h := &hasher{
|
||||||
|
bw: bw,
|
||||||
|
visited: map[uintptr]bool{},
|
||||||
|
}
|
||||||
|
h.print(reflect.ValueOf(x))
|
||||||
|
bw.Flush()
|
||||||
|
const want = "struct" +
|
||||||
|
"\x00\x00\x00\x00\x00\x00\x00\x01" + // 1 field
|
||||||
|
"\x00\x00\x00\x00\x00\x00\x00\x00" + // 0th field
|
||||||
|
// the 32 bytes:
|
||||||
|
"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f"
|
||||||
|
if got := got.Bytes(); string(got) != want {
|
||||||
|
t.Errorf("wrong:\n got: %q\nwant: %q\n", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkHashMapAcyclic(b *testing.B) {
|
func BenchmarkHashMapAcyclic(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
m := map[int]string{}
|
m := map[int]string{}
|
||||||
@ -238,3 +261,28 @@ type T struct {
|
|||||||
v.M["m"] = v.M
|
v.M["m"] = v.M
|
||||||
Hash(v)
|
Hash(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArrayAllocs(t *testing.T) {
|
||||||
|
type T struct {
|
||||||
|
X [32]byte
|
||||||
|
}
|
||||||
|
x := &T{X: [32]byte{1: 1, 2: 2, 3: 3, 4: 4}}
|
||||||
|
n := int(testing.AllocsPerRun(1000, func() {
|
||||||
|
sink = Hash(x)
|
||||||
|
}))
|
||||||
|
if n > 0 {
|
||||||
|
t.Errorf("allocs = %v; want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHashArray(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
type T struct {
|
||||||
|
X [32]byte
|
||||||
|
}
|
||||||
|
x := &T{X: [32]byte{1: 1, 2: 2, 3: 3, 4: 4}}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
sink = Hash(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user