util/deephash: export a Hash func for use by the control plane

name              old time/op    new time/op    delta
Hash-6              69.4µs ± 6%    68.4µs ± 4%     ~     (p=0.286 n=9+9)
HashMapAcyclic-6     115µs ± 5%     115µs ± 4%     ~     (p=1.000 n=10+10)

name              old alloc/op   new alloc/op   delta
Hash-6              2.29kB ± 0%    1.88kB ± 0%  -18.13%  (p=0.000 n=10+10)
HashMapAcyclic-6    2.53kB ± 0%    2.53kB ± 0%     ~     (all equal)

name              old allocs/op  new allocs/op  delta
Hash-6                58.0 ± 0%      54.0 ± 0%   -6.90%  (p=0.000 n=10+10)
HashMapAcyclic-6       202 ± 0%       202 ± 0%     ~     (all equal)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-07-04 21:25:15 -07:00 committed by Brad Fitzpatrick
parent 700badd8f8
commit 9ae3bd0939
2 changed files with 79 additions and 24 deletions

View File

@ -20,29 +20,67 @@
"sync"
)
func calcHash(v interface{}) string {
h := sha256.New()
b := bufio.NewWriterSize(h, h.BlockSize())
scratch := make([]byte, 0, 128)
printTo(b, v, scratch)
b.Flush()
scratch = h.Sum(scratch[:0])
// The first sha256.Size bytes contain the hash.
// Hex-encode that into the next sha256.Size*2 bytes.
src := scratch[:sha256.Size]
dst := scratch[sha256.Size:cap(scratch)]
n := hex.Encode(dst, src)
return string(dst[:n])
// hasher is reusable state for hashing a value.
// Get one via hasherPool.
type hasher struct {
h hash.Hash
bw *bufio.Writer
scratch [128]byte
}
// UpdateHash sets last to the hash of v and reports whether its value changed.
// newHasher initializes a new hasher, for use by hasherPool.
func newHasher() *hasher {
h := &hasher{h: sha256.New()}
h.bw = bufio.NewWriterSize(h.h, h.h.BlockSize())
return h
}
// Hash returns the raw SHA-256 (not hex) of v.
func (h *hasher) Hash(v interface{}) (hash [sha256.Size]byte) {
h.bw.Flush()
h.h.Reset()
printTo(h.bw, v, h.scratch[:])
h.bw.Flush()
h.h.Sum(hash[:0])
return hash
}
var hasherPool = &sync.Pool{
New: func() interface{} { return newHasher() },
}
// Hash returns the raw SHA-256 hash of v.
func Hash(v interface{}) [sha256.Size]byte {
hasher := hasherPool.Get().(*hasher)
hasherPool.Put(hasher)
return hasher.Hash(v)
}
// UpdateHash sets last to the hex-encoded hash of v and reports whether its value changed.
func UpdateHash(last *string, v ...interface{}) (changed bool) {
sig := calcHash(v)
if *last != sig {
*last = sig
return true
sum := Hash(v)
if sha256EqualHex(sum, *last) {
// unchanged.
return false
}
return false
*last = hex.EncodeToString(sum[:])
return true
}
// sha256EqualHex reports whether hx is the hex encoding of sum.
func sha256EqualHex(sum [sha256.Size]byte, hx string) bool {
if len(hx) != len(sum)*2 {
return false
}
const hextable = "0123456789abcdef"
j := 0
for _, v := range sum {
if hx[j] != hextable[v>>4] || hx[j+1] != hextable[v&0x0f] {
return false
}
j += 2
}
return true
}
func printTo(w *bufio.Writer, v interface{}, scratch []byte) {

View File

@ -7,6 +7,8 @@
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"reflect"
"testing"
@ -23,10 +25,10 @@ func TestDeepHash(t *testing.T) {
// Mostly we're just testing that we don't panic on handled types.
v := getVal()
hash1 := calcHash(v)
hash1 := Hash(v)
t.Logf("hash: %v", hash1)
for i := 0; i < 20; i++ {
hash2 := calcHash(getVal())
hash2 := Hash(getVal())
if hash1 != hash2 {
t.Error("second hash didn't match")
}
@ -76,11 +78,13 @@ func getVal() []interface{} {
}
}
var sink = Hash("foo")
func BenchmarkHash(b *testing.B) {
b.ReportAllocs()
v := getVal()
for i := 0; i < b.N; i++ {
calcHash(v)
sink = Hash(v)
}
}
@ -136,12 +140,25 @@ func BenchmarkHashMapAcyclic(b *testing.B) {
}
func TestExhaustive(t *testing.T) {
seen := make(map[string]bool)
seen := make(map[[sha256.Size]byte]bool)
for i := 0; i < 100000; i++ {
s := calcHash(i)
s := Hash(i)
if seen[s] {
t.Fatalf("hash collision %v", i)
}
seen[s] = true
}
}
func TestSHA256EqualHex(t *testing.T) {
for i := 0; i < 1000; i++ {
sum := Hash(i)
hx := hex.EncodeToString(sum[:])
if !sha256EqualHex(sum, hx) {
t.Fatal("didn't match, should've")
}
if sha256EqualHex(sum, hx[:len(hx)-1]) {
t.Fatal("matched on wrong length")
}
}
}