From 9ae3bd09394c8d41bc065fdf8856739b468f51f7 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 4 Jul 2021 21:25:15 -0700 Subject: [PATCH] util/deephash: export a Hash func for use by the control plane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- util/deephash/deephash.go | 80 +++++++++++++++++++++++++--------- util/deephash/deephash_test.go | 27 +++++++++--- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index c116f0977..3f1b8ac94 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -20,29 +20,67 @@ import ( "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]) -} - -// UpdateHash sets last to the hash of v and reports whether its value changed. +// hasher is reusable state for hashing a value. +// Get one via hasherPool. +type hasher struct { + h hash.Hash + bw *bufio.Writer + scratch [128]byte +} + +// 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 + } + *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 } - 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) { diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 5b5e9a24c..4b01e090f 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -7,6 +7,8 @@ package deephash 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") + } + } +}