mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 14:05:39 +00:00
2a22ea3e83
name old time/op new time/op delta Hash-8 71.1µs ± 2% 71.5µs ± 1% ~ (p=0.114 n=9+8) HashPacketFilter-8 8.39µs ± 1% 4.83µs ± 2% -42.38% (p=0.000 n=8+9) HashMapAcyclic-8 56.2µs ± 1% 56.9µs ± 2% +1.17% (p=0.035 n=10+9) TailcfgNode-8 6.49µs ± 2% 3.54µs ± 1% -45.37% (p=0.000 n=9+9) HashArray-8 729ns ± 2% 566ns ± 3% -22.30% (p=0.000 n=10+10) name old alloc/op new alloc/op delta Hash-8 24.0B ± 0% 24.0B ± 0% ~ (all equal) HashPacketFilter-8 24.0B ± 0% 24.0B ± 0% ~ (all equal) HashMapAcyclic-8 0.00B 0.00B ~ (all equal) TailcfgNode-8 0.00B 0.00B ~ (all equal) HashArray-8 0.00B 0.00B ~ (all equal) name old allocs/op new allocs/op delta Hash-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) HashPacketFilter-8 1.00 ± 0% 1.00 ± 0% ~ (all equal) HashMapAcyclic-8 0.00 0.00 ~ (all equal) TailcfgNode-8 0.00 0.00 ~ (all equal) HashArray-8 0.00 0.00 ~ (all equal) Change-Id: I34c4e786e748fe60280646d40cc63a2adb2ea6fe Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
998 lines
27 KiB
Go
998 lines
27 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package deephash hashes a Go value recursively, in a predictable order,
|
|
// without looping. The hash is only valid within the lifetime of a program.
|
|
// Users should not store the hash on disk or send it over the network.
|
|
// The hash is sufficiently strong and unique such that
|
|
// Hash(x) == Hash(y) is an appropriate replacement for x == y.
|
|
//
|
|
// The definition of equality is identical to reflect.DeepEqual except:
|
|
// * Floating-point values are compared based on the raw bits,
|
|
// which means that NaNs (with the same bit pattern) are treated as equal.
|
|
// * Types which implement interface { AppendTo([]byte) []byte } use
|
|
// the AppendTo method to produce a textual representation of the value.
|
|
// Thus, two values are equal if AppendTo produces the same bytes.
|
|
//
|
|
// WARNING: This package, like most of the tailscale.com Go module,
|
|
// should be considered Tailscale-internal; we make no API promises.
|
|
package deephash
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"hash"
|
|
"log"
|
|
"math"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
)
|
|
|
|
// There is much overlap between the theory of serialization and hashing.
|
|
// A hash (useful for determining equality) can be produced by printing a value
|
|
// and hashing the output. The format must:
|
|
// * be deterministic such that the same value hashes to the same output, and
|
|
// * be parsable such that the same value can be reproduced by the output.
|
|
//
|
|
// The logic below hashes a value by printing it to a hash.Hash.
|
|
// To be parsable, it assumes that we know the Go type of each value:
|
|
// * scalar types (e.g., bool or int32) are printed as fixed-width fields.
|
|
// * list types (e.g., strings, slices, and AppendTo buffers) are prefixed
|
|
// by a fixed-width length field, followed by the contents of the list.
|
|
// * slices, arrays, and structs print each element/field consecutively.
|
|
// * interfaces print with a 1-byte prefix indicating whether it is nil.
|
|
// If non-nil, it is followed by a fixed-width field of the type index,
|
|
// followed by the format of the underlying value.
|
|
// * pointers print with a 1-byte prefix indicating whether the pointer is
|
|
// 1) nil, 2) previously seen, or 3) newly seen. Previously seen pointers are
|
|
// followed by a fixed-width field with the index of the previous pointer.
|
|
// Newly seen pointers are followed by the format of the underlying value.
|
|
// * maps print with a 1-byte prefix indicating whether the map pointer is
|
|
// 1) nil, 2) previously seen, or 3) newly seen. Previously seen pointers
|
|
// are followed by a fixed-width field of the index of the previous pointer.
|
|
// Newly seen maps are printed as a fixed-width field with the XOR of the
|
|
// hash of every map entry. With a sufficiently strong hash, this value is
|
|
// theoretically "parsable" by looking up the hash in a magical map that
|
|
// returns the set of entries for that given hash.
|
|
|
|
const scratchSize = 128
|
|
|
|
// hasher is reusable state for hashing a value.
|
|
// Get one via hasherPool.
|
|
type hasher struct {
|
|
h hash.Hash
|
|
bw *bufio.Writer
|
|
scratch [scratchSize]byte
|
|
visitStack visitStack
|
|
}
|
|
|
|
func (h *hasher) reset() {
|
|
if h.h == nil {
|
|
h.h = sha256.New()
|
|
}
|
|
if h.bw == nil {
|
|
h.bw = bufio.NewWriterSize(h.h, h.h.BlockSize())
|
|
}
|
|
h.bw.Flush()
|
|
h.h.Reset()
|
|
}
|
|
|
|
// Sum is an opaque checksum type that is comparable.
|
|
type Sum struct {
|
|
sum [sha256.Size]byte
|
|
}
|
|
|
|
func (s1 *Sum) xor(s2 Sum) {
|
|
for i := 0; i < sha256.Size; i++ {
|
|
s1.sum[i] ^= s2.sum[i]
|
|
}
|
|
}
|
|
|
|
func (s Sum) String() string {
|
|
return hex.EncodeToString(s.sum[:])
|
|
}
|
|
|
|
var (
|
|
seedOnce sync.Once
|
|
seed uint64
|
|
)
|
|
|
|
func initSeed() {
|
|
seed = uint64(time.Now().UnixNano())
|
|
}
|
|
|
|
func (h *hasher) sum() (s Sum) {
|
|
h.bw.Flush()
|
|
// Sum into scratch & copy out, as hash.Hash is an interface
|
|
// 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:
|
|
copy(s.sum[:], h.h.Sum(h.scratch[:0]))
|
|
return s
|
|
}
|
|
|
|
var hasherPool = &sync.Pool{
|
|
New: func() any { return new(hasher) },
|
|
}
|
|
|
|
// Hash returns the hash of v.
|
|
func Hash(v any) (s Sum) {
|
|
h := hasherPool.Get().(*hasher)
|
|
defer hasherPool.Put(h)
|
|
h.reset()
|
|
seedOnce.Do(initSeed)
|
|
h.hashUint64(seed)
|
|
|
|
rv := reflect.ValueOf(v)
|
|
if rv.IsValid() {
|
|
// Always treat the Hash input as an interface (it is), including hashing
|
|
// its type, otherwise two Hash calls of different types could hash to the
|
|
// same bytes off the different types and get equivalent Sum values. This is
|
|
// the same thing that we do for reflect.Kind Interface in hashValue, but
|
|
// the initial reflect.ValueOf from an interface value effectively strips
|
|
// the interface box off so we have to do it at the top level by hand.
|
|
h.hashType(rv.Type())
|
|
h.hashValue(rv, false)
|
|
}
|
|
return h.sum()
|
|
}
|
|
|
|
// HasherForType is like Hash, but it returns a Hash func that's specialized for
|
|
// the provided reflect type, avoiding a map lookup per value.
|
|
func HasherForType[T any]() func(T) Sum {
|
|
var zeroT T
|
|
ti := getTypeInfo(reflect.TypeOf(zeroT))
|
|
seedOnce.Do(initSeed)
|
|
|
|
return func(v T) Sum {
|
|
h := hasherPool.Get().(*hasher)
|
|
defer hasherPool.Put(h)
|
|
h.reset()
|
|
h.hashUint64(seed)
|
|
|
|
rv := reflect.ValueOf(v)
|
|
|
|
if rv.IsValid() {
|
|
// Always treat the Hash input as an interface (it is), including hashing
|
|
// its type, otherwise two Hash calls of different types could hash to the
|
|
// same bytes off the different types and get equivalent Sum values. This is
|
|
// the same thing that we do for reflect.Kind Interface in hashValue, but
|
|
// the initial reflect.ValueOf from an interface value effectively strips
|
|
// the interface box off so we have to do it at the top level by hand.
|
|
h.hashType(rv.Type())
|
|
h.hashValueWithType(rv, ti, false)
|
|
}
|
|
return h.sum()
|
|
}
|
|
}
|
|
|
|
// Update sets last to the hash of v and reports whether its value changed.
|
|
func Update(last *Sum, v ...any) (changed bool) {
|
|
sum := Hash(v)
|
|
if sum == *last {
|
|
// unchanged.
|
|
return false
|
|
}
|
|
*last = sum
|
|
return true
|
|
}
|
|
|
|
var appenderToType = reflect.TypeOf((*appenderTo)(nil)).Elem()
|
|
|
|
type appenderTo interface {
|
|
AppendTo([]byte) []byte
|
|
}
|
|
|
|
func (h *hasher) hashUint8(i uint8) {
|
|
h.bw.WriteByte(i)
|
|
}
|
|
func (h *hasher) hashUint16(i uint16) {
|
|
binary.LittleEndian.PutUint16(h.scratch[:2], i)
|
|
h.bw.Write(h.scratch[:2])
|
|
}
|
|
func (h *hasher) hashUint32(i uint32) {
|
|
binary.LittleEndian.PutUint32(h.scratch[:4], i)
|
|
h.bw.Write(h.scratch[:4])
|
|
}
|
|
func (h *hasher) hashLen(n int) {
|
|
binary.LittleEndian.PutUint64(h.scratch[:8], uint64(n))
|
|
h.bw.Write(h.scratch[:8])
|
|
}
|
|
func (h *hasher) hashUint64(i uint64) {
|
|
binary.LittleEndian.PutUint64(h.scratch[:8], i)
|
|
h.bw.Write(h.scratch[:8])
|
|
}
|
|
|
|
var (
|
|
uint8Type = reflect.TypeOf(byte(0))
|
|
timeTimeType = reflect.TypeOf(time.Time{})
|
|
)
|
|
|
|
// typeInfo describes properties of a type.
|
|
//
|
|
// A non-nil typeInfo is populated into the typeHasher map
|
|
// when its type is first requested, before its func is created.
|
|
// Its func field fn is only populated once the type has been created.
|
|
// This is used for recursive types.
|
|
type typeInfo struct {
|
|
rtype reflect.Type
|
|
canMemHash bool
|
|
isRecursive bool
|
|
|
|
// elemTypeInfo is the element type's typeInfo.
|
|
// It's set when rtype is of Kind Ptr, Slice, Array, Map.
|
|
elemTypeInfo *typeInfo
|
|
|
|
// keyTypeInfo is the map key type's typeInfo.
|
|
// It's set when rtype is of Kind Map.
|
|
keyTypeInfo *typeInfo
|
|
|
|
hashFuncOnce sync.Once
|
|
hashFuncLazy typeHasherFunc // nil until created
|
|
}
|
|
|
|
// returns ok if it was handled; else slow path runs
|
|
type typeHasherFunc func(h *hasher, v reflect.Value) (ok bool)
|
|
|
|
var typeInfoMap sync.Map // map[reflect.Type]*typeInfo
|
|
var typeInfoMapPopulate sync.Mutex // just for adding to typeInfoMap
|
|
|
|
func (ti *typeInfo) hasher() typeHasherFunc {
|
|
ti.hashFuncOnce.Do(ti.buildHashFuncOnce)
|
|
return ti.hashFuncLazy
|
|
}
|
|
|
|
func (ti *typeInfo) buildHashFuncOnce() {
|
|
ti.hashFuncLazy = genTypeHasher(ti.rtype)
|
|
}
|
|
|
|
func (h *hasher) hashBoolv(v reflect.Value) bool {
|
|
var b byte
|
|
if v.Bool() {
|
|
b = 1
|
|
}
|
|
h.hashUint8(b)
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashUint8v(v reflect.Value) bool {
|
|
h.hashUint8(uint8(v.Uint()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashInt8v(v reflect.Value) bool {
|
|
h.hashUint8(uint8(v.Int()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashUint16v(v reflect.Value) bool {
|
|
h.hashUint16(uint16(v.Uint()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashInt16v(v reflect.Value) bool {
|
|
h.hashUint16(uint16(v.Int()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashUint32v(v reflect.Value) bool {
|
|
h.hashUint32(uint32(v.Uint()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashInt32v(v reflect.Value) bool {
|
|
h.hashUint32(uint32(v.Int()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashUint64v(v reflect.Value) bool {
|
|
h.hashUint64(v.Uint())
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashInt64v(v reflect.Value) bool {
|
|
h.hashUint64(uint64(v.Int()))
|
|
return true
|
|
}
|
|
|
|
func hashStructAppenderTo(h *hasher, v reflect.Value) bool {
|
|
if !v.CanInterface() {
|
|
return false // slow path
|
|
}
|
|
var a appenderTo
|
|
if v.CanAddr() {
|
|
a = v.Addr().Interface().(appenderTo)
|
|
} else {
|
|
a = v.Interface().(appenderTo)
|
|
}
|
|
size := h.scratch[:8]
|
|
record := a.AppendTo(size)
|
|
binary.LittleEndian.PutUint64(record, uint64(len(record)-len(size)))
|
|
h.bw.Write(record)
|
|
return true
|
|
}
|
|
|
|
// hashPointerAppenderTo hashes v, a reflect.Ptr, that implements appenderTo.
|
|
func hashPointerAppenderTo(h *hasher, v reflect.Value) bool {
|
|
if !v.CanInterface() {
|
|
return false // slow path
|
|
}
|
|
if v.IsNil() {
|
|
h.hashUint8(0) // indicates nil
|
|
return true
|
|
}
|
|
h.hashUint8(1) // indicates visiting a pointer
|
|
a := v.Interface().(appenderTo)
|
|
size := h.scratch[:8]
|
|
record := a.AppendTo(size)
|
|
binary.LittleEndian.PutUint64(record, uint64(len(record)-len(size)))
|
|
h.bw.Write(record)
|
|
return true
|
|
}
|
|
|
|
// fieldInfo describes a struct field.
|
|
type fieldInfo struct {
|
|
index int // index of field for reflect.Value.Field(n)
|
|
typeInfo *typeInfo
|
|
canMemHash bool
|
|
offset uintptr // when we can memhash the field
|
|
size uintptr // when we can memhash the field
|
|
}
|
|
|
|
// mergeContiguousFieldsCopy returns a copy of f with contiguous memhashable fields
|
|
// merged together. Such fields get a bogus index and fu value.
|
|
func mergeContiguousFieldsCopy(in []fieldInfo) []fieldInfo {
|
|
ret := make([]fieldInfo, 0, len(in))
|
|
var last *fieldInfo
|
|
for _, f := range in {
|
|
// Combine two fields if they're both contiguous & memhash-able.
|
|
if f.canMemHash && last != nil && last.canMemHash && last.offset+last.size == f.offset {
|
|
last.size += f.size
|
|
last.index = -1
|
|
last.typeInfo = nil
|
|
} else {
|
|
ret = append(ret, f)
|
|
last = &ret[len(ret)-1]
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// genHashStructFields generates a typeHasherFunc for t, which must be of kind Struct.
|
|
func genHashStructFields(t reflect.Type) typeHasherFunc {
|
|
fields := make([]fieldInfo, 0, t.NumField())
|
|
for i, n := 0, t.NumField(); i < n; i++ {
|
|
sf := t.Field(i)
|
|
if sf.Type.Size() == 0 {
|
|
continue
|
|
}
|
|
fields = append(fields, fieldInfo{
|
|
index: i,
|
|
typeInfo: getTypeInfo(sf.Type),
|
|
canMemHash: canMemHash(sf.Type),
|
|
offset: sf.Offset,
|
|
size: sf.Type.Size(),
|
|
})
|
|
}
|
|
fieldsIfCanAddr := mergeContiguousFieldsCopy(fields)
|
|
return structHasher{fields, fieldsIfCanAddr}.hash
|
|
}
|
|
|
|
type structHasher struct {
|
|
fields, fieldsIfCanAddr []fieldInfo
|
|
}
|
|
|
|
func (sh structHasher) hash(h *hasher, v reflect.Value) bool {
|
|
var base unsafe.Pointer
|
|
if v.CanAddr() {
|
|
base = v.Addr().UnsafePointer()
|
|
for _, f := range sh.fieldsIfCanAddr {
|
|
if f.canMemHash {
|
|
h.bw.Write(unsafe.Slice((*byte)(unsafe.Pointer(uintptr(base)+f.offset)), f.size))
|
|
} else if !f.typeInfo.hasher()(h, v.Field(f.index)) {
|
|
return false
|
|
}
|
|
}
|
|
} else {
|
|
for _, f := range sh.fields {
|
|
if !f.typeInfo.hasher()(h, v.Field(f.index)) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// genHashPtrToMemoryRange returns a hasher where the reflect.Value is a Ptr to
|
|
// the provided eleType.
|
|
func genHashPtrToMemoryRange(eleType reflect.Type) typeHasherFunc {
|
|
size := eleType.Size()
|
|
return func(h *hasher, v reflect.Value) bool {
|
|
if v.IsNil() {
|
|
h.hashUint8(0) // indicates nil
|
|
} else {
|
|
h.hashUint8(1) // indicates visiting a pointer
|
|
h.bw.Write(unsafe.Slice((*byte)(v.UnsafePointer()), size))
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
const debug = false
|
|
|
|
func genTypeHasher(t reflect.Type) typeHasherFunc {
|
|
if debug {
|
|
log.Printf("generating func for %v", t)
|
|
}
|
|
|
|
switch t.Kind() {
|
|
case reflect.Bool:
|
|
return (*hasher).hashBoolv
|
|
case reflect.Int8:
|
|
return (*hasher).hashInt8v
|
|
case reflect.Int16:
|
|
return (*hasher).hashInt16v
|
|
case reflect.Int32:
|
|
return (*hasher).hashInt32v
|
|
case reflect.Int, reflect.Int64:
|
|
return (*hasher).hashInt64v
|
|
case reflect.Uint8:
|
|
return (*hasher).hashUint8v
|
|
case reflect.Uint16:
|
|
return (*hasher).hashUint16v
|
|
case reflect.Uint32:
|
|
return (*hasher).hashUint32v
|
|
case reflect.Uint, reflect.Uintptr, reflect.Uint64:
|
|
return (*hasher).hashUint64v
|
|
case reflect.Float32:
|
|
return (*hasher).hashFloat32v
|
|
case reflect.Float64:
|
|
return (*hasher).hashFloat64v
|
|
case reflect.Complex64:
|
|
return (*hasher).hashComplex64v
|
|
case reflect.Complex128:
|
|
return (*hasher).hashComplex128v
|
|
case reflect.String:
|
|
return (*hasher).hashString
|
|
case reflect.Slice:
|
|
et := t.Elem()
|
|
if canMemHash(et) {
|
|
return (*hasher).hashSliceMem
|
|
}
|
|
eti := getTypeInfo(et)
|
|
return genHashSliceElements(eti)
|
|
case reflect.Array:
|
|
et := t.Elem()
|
|
eti := getTypeInfo(et)
|
|
return genHashArray(t, eti)
|
|
case reflect.Struct:
|
|
if t == timeTimeType {
|
|
return (*hasher).hashTimev
|
|
}
|
|
if t.Implements(appenderToType) {
|
|
return hashStructAppenderTo
|
|
}
|
|
return genHashStructFields(t)
|
|
case reflect.Pointer:
|
|
et := t.Elem()
|
|
if canMemHash(et) {
|
|
return genHashPtrToMemoryRange(et)
|
|
}
|
|
if t.Implements(appenderToType) {
|
|
return hashPointerAppenderTo
|
|
}
|
|
if !typeIsRecursive(t) {
|
|
eti := getTypeInfo(et)
|
|
return func(h *hasher, v reflect.Value) bool {
|
|
if v.IsNil() {
|
|
h.hashUint8(0) // indicates nil
|
|
return true
|
|
}
|
|
h.hashUint8(1) // indicates visiting a pointer
|
|
return eti.hasher()(h, v.Elem())
|
|
}
|
|
}
|
|
}
|
|
|
|
return func(h *hasher, v reflect.Value) bool {
|
|
if debug {
|
|
log.Printf("unhandled type %v", v.Type())
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// hashString hashes v, of kind String.
|
|
func (h *hasher) hashString(v reflect.Value) bool {
|
|
s := v.String()
|
|
h.hashLen(len(s))
|
|
h.bw.WriteString(s)
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashFloat32v(v reflect.Value) bool {
|
|
h.hashUint32(math.Float32bits(float32(v.Float())))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashFloat64v(v reflect.Value) bool {
|
|
h.hashUint64(math.Float64bits(v.Float()))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashComplex64v(v reflect.Value) bool {
|
|
c := complex64(v.Complex())
|
|
h.hashUint32(math.Float32bits(real(c)))
|
|
h.hashUint32(math.Float32bits(imag(c)))
|
|
return true
|
|
}
|
|
|
|
func (h *hasher) hashComplex128v(v reflect.Value) bool {
|
|
c := v.Complex()
|
|
h.hashUint64(math.Float64bits(real(c)))
|
|
h.hashUint64(math.Float64bits(imag(c)))
|
|
return true
|
|
}
|
|
|
|
// hashString hashes v, of kind time.Time.
|
|
func (h *hasher) hashTimev(v reflect.Value) bool {
|
|
var t time.Time
|
|
if v.CanAddr() {
|
|
t = *(v.Addr().Interface().(*time.Time))
|
|
} else {
|
|
t = v.Interface().(time.Time)
|
|
}
|
|
b := t.AppendFormat(h.scratch[:1], time.RFC3339Nano)
|
|
b[0] = byte(len(b) - 1) // more than sufficient width; if not, good enough.
|
|
h.bw.Write(b)
|
|
return true
|
|
}
|
|
|
|
// hashSliceMem hashes v, of kind Slice, with a memhash-able element type.
|
|
func (h *hasher) hashSliceMem(v reflect.Value) bool {
|
|
vLen := v.Len()
|
|
h.hashUint64(uint64(vLen))
|
|
if vLen == 0 {
|
|
return true
|
|
}
|
|
h.bw.Write(unsafe.Slice((*byte)(v.UnsafePointer()), v.Type().Elem().Size()*uintptr(vLen)))
|
|
return true
|
|
}
|
|
|
|
func genHashArrayMem(n int, arraySize uintptr, efu *typeInfo) typeHasherFunc {
|
|
byElement := genHashArrayElements(n, efu)
|
|
return func(h *hasher, v reflect.Value) bool {
|
|
if v.CanAddr() {
|
|
h.bw.Write(unsafe.Slice((*byte)(v.Addr().UnsafePointer()), arraySize))
|
|
return true
|
|
}
|
|
return byElement(h, v)
|
|
}
|
|
}
|
|
|
|
func genHashArrayElements(n int, eti *typeInfo) typeHasherFunc {
|
|
return func(h *hasher, v reflect.Value) bool {
|
|
for i := 0; i < n; i++ {
|
|
if !eti.hasher()(h, v.Index(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
func noopHasherFunc(h *hasher, v reflect.Value) bool { return true }
|
|
|
|
func genHashArray(t reflect.Type, eti *typeInfo) typeHasherFunc {
|
|
if t.Size() == 0 {
|
|
return noopHasherFunc
|
|
}
|
|
et := t.Elem()
|
|
if canMemHash(et) {
|
|
return genHashArrayMem(t.Len(), t.Size(), eti)
|
|
}
|
|
n := t.Len()
|
|
return genHashArrayElements(n, eti)
|
|
}
|
|
|
|
func genHashSliceElements(eti *typeInfo) typeHasherFunc {
|
|
return sliceElementHasher{eti}.hash
|
|
}
|
|
|
|
type sliceElementHasher struct {
|
|
eti *typeInfo
|
|
}
|
|
|
|
func (seh sliceElementHasher) hash(h *hasher, v reflect.Value) bool {
|
|
vLen := v.Len()
|
|
h.hashUint64(uint64(vLen))
|
|
for i := 0; i < vLen; i++ {
|
|
if !seh.eti.hasher()(h, v.Index(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func getTypeInfo(t reflect.Type) *typeInfo {
|
|
if f, ok := typeInfoMap.Load(t); ok {
|
|
return f.(*typeInfo)
|
|
}
|
|
typeInfoMapPopulate.Lock()
|
|
defer typeInfoMapPopulate.Unlock()
|
|
newTypes := map[reflect.Type]*typeInfo{}
|
|
ti := getTypeInfoLocked(t, newTypes)
|
|
for t, ti := range newTypes {
|
|
typeInfoMap.Store(t, ti)
|
|
}
|
|
return ti
|
|
}
|
|
|
|
func getTypeInfoLocked(t reflect.Type, incomplete map[reflect.Type]*typeInfo) *typeInfo {
|
|
if v, ok := typeInfoMap.Load(t); ok {
|
|
return v.(*typeInfo)
|
|
}
|
|
if ti, ok := incomplete[t]; ok {
|
|
return ti
|
|
}
|
|
ti := &typeInfo{
|
|
rtype: t,
|
|
isRecursive: typeIsRecursive(t),
|
|
canMemHash: canMemHash(t),
|
|
}
|
|
incomplete[t] = ti
|
|
|
|
switch t.Kind() {
|
|
case reflect.Map:
|
|
ti.keyTypeInfo = getTypeInfoLocked(t.Key(), incomplete)
|
|
fallthrough
|
|
case reflect.Ptr, reflect.Slice, reflect.Array:
|
|
ti.elemTypeInfo = getTypeInfoLocked(t.Elem(), incomplete)
|
|
}
|
|
|
|
return ti
|
|
}
|
|
|
|
// typeIsRecursive reports whether t has a path back to itself.
|
|
//
|
|
// For interfaces, it currently always reports true.
|
|
func typeIsRecursive(t reflect.Type) bool {
|
|
inStack := map[reflect.Type]bool{}
|
|
|
|
var stack []reflect.Type
|
|
|
|
var visitType func(t reflect.Type) (isRecursiveSoFar bool)
|
|
visitType = func(t reflect.Type) (isRecursiveSoFar bool) {
|
|
switch t.Kind() {
|
|
case reflect.Bool,
|
|
reflect.Int,
|
|
reflect.Int8,
|
|
reflect.Int16,
|
|
reflect.Int32,
|
|
reflect.Int64,
|
|
reflect.Uint,
|
|
reflect.Uint8,
|
|
reflect.Uint16,
|
|
reflect.Uint32,
|
|
reflect.Uint64,
|
|
reflect.Uintptr,
|
|
reflect.Float32,
|
|
reflect.Float64,
|
|
reflect.Complex64,
|
|
reflect.Complex128,
|
|
reflect.String,
|
|
reflect.UnsafePointer,
|
|
reflect.Func:
|
|
return false
|
|
}
|
|
if t.Size() == 0 {
|
|
return false
|
|
}
|
|
if inStack[t] {
|
|
return true
|
|
}
|
|
stack = append(stack, t)
|
|
inStack[t] = true
|
|
defer func() {
|
|
delete(inStack, t)
|
|
stack = stack[:len(stack)-1]
|
|
}()
|
|
|
|
switch t.Kind() {
|
|
default:
|
|
panic("unhandled kind " + t.Kind().String())
|
|
case reflect.Interface:
|
|
// Assume the worst for now. TODO(bradfitz): in some cases
|
|
// we should be able to prove that it's not recursive. Not worth
|
|
// it for now.
|
|
return true
|
|
case reflect.Array, reflect.Chan, reflect.Pointer, reflect.Slice:
|
|
return visitType(t.Elem())
|
|
case reflect.Map:
|
|
if visitType(t.Key()) {
|
|
return true
|
|
}
|
|
if visitType(t.Elem()) {
|
|
return true
|
|
}
|
|
case reflect.Struct:
|
|
if t.String() == "intern.Value" {
|
|
// Otherwise its interface{} makes this return true.
|
|
return false
|
|
}
|
|
for i, numField := 0, t.NumField(); i < numField; i++ {
|
|
if visitType(t.Field(i).Type) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
return visitType(t)
|
|
}
|
|
|
|
// canMemHash reports whether a slice of t can be hashed by looking at its
|
|
// contiguous bytes in memory alone. (e.g. structs with gaps aren't memhashable)
|
|
func canMemHash(t reflect.Type) bool {
|
|
switch t.Kind() {
|
|
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
|
reflect.Float64, reflect.Float32, reflect.Complex128, reflect.Complex64:
|
|
return true
|
|
case reflect.Array:
|
|
return canMemHash(t.Elem())
|
|
case reflect.Struct:
|
|
var sumFieldSize uintptr
|
|
for i, numField := 0, t.NumField(); i < numField; i++ {
|
|
sf := t.Field(i)
|
|
if !canMemHash(sf.Type) {
|
|
// Special case for 0-width fields that aren't at the end.
|
|
if sf.Type.Size() == 0 && i < numField-1 {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
sumFieldSize += sf.Type.Size()
|
|
}
|
|
return sumFieldSize == t.Size() // else there are gaps
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *hasher) hashValue(v reflect.Value, forceCycleChecking bool) {
|
|
if !v.IsValid() {
|
|
return
|
|
}
|
|
ti := getTypeInfo(v.Type())
|
|
h.hashValueWithType(v, ti, forceCycleChecking)
|
|
}
|
|
|
|
func (h *hasher) hashValueWithType(v reflect.Value, ti *typeInfo, forceCycleChecking bool) {
|
|
w := h.bw
|
|
doCheckCycles := forceCycleChecking || ti.isRecursive
|
|
|
|
if !doCheckCycles {
|
|
hf := ti.hasher()
|
|
if hf(h, v) {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Generic handling.
|
|
switch v.Kind() {
|
|
default:
|
|
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
|
case reflect.Ptr:
|
|
if v.IsNil() {
|
|
h.hashUint8(0) // indicates nil
|
|
return
|
|
}
|
|
|
|
if doCheckCycles {
|
|
ptr := pointerOf(v)
|
|
if idx, ok := h.visitStack.seen(ptr); ok {
|
|
h.hashUint8(2) // indicates cycle
|
|
h.hashUint64(uint64(idx))
|
|
return
|
|
}
|
|
h.visitStack.push(ptr)
|
|
defer h.visitStack.pop(ptr)
|
|
}
|
|
|
|
h.hashUint8(1) // indicates visiting a pointer
|
|
h.hashValueWithType(v.Elem(), ti.elemTypeInfo, doCheckCycles)
|
|
case reflect.Struct:
|
|
for i, n := 0, v.NumField(); i < n; i++ {
|
|
h.hashValue(v.Field(i), doCheckCycles)
|
|
}
|
|
case reflect.Slice, reflect.Array:
|
|
vLen := v.Len()
|
|
if v.Kind() == reflect.Slice {
|
|
h.hashUint64(uint64(vLen))
|
|
}
|
|
if v.Type().Elem() == uint8Type && v.CanInterface() {
|
|
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
|
|
}
|
|
fmt.Fprintf(w, "%s", v.Interface())
|
|
return
|
|
}
|
|
for i := 0; i < vLen; i++ {
|
|
// TODO(dsnet): Perform cycle detection for slices,
|
|
// which is functionally a list of pointers.
|
|
// See https://github.com/google/go-cmp/blob/402949e8139bb890c71a707b6faf6dd05c92f4e5/cmp/compare.go#L438-L450
|
|
h.hashValueWithType(v.Index(i), ti.elemTypeInfo, doCheckCycles)
|
|
}
|
|
case reflect.Interface:
|
|
if v.IsNil() {
|
|
h.hashUint8(0) // indicates nil
|
|
return
|
|
}
|
|
v = v.Elem()
|
|
|
|
h.hashUint8(1) // indicates visiting interface value
|
|
h.hashType(v.Type())
|
|
h.hashValue(v, doCheckCycles)
|
|
case reflect.Map:
|
|
// Check for cycle.
|
|
if doCheckCycles {
|
|
ptr := pointerOf(v)
|
|
if idx, ok := h.visitStack.seen(ptr); ok {
|
|
h.hashUint8(2) // indicates cycle
|
|
h.hashUint64(uint64(idx))
|
|
return
|
|
}
|
|
h.visitStack.push(ptr)
|
|
defer h.visitStack.pop(ptr)
|
|
}
|
|
h.hashUint8(1) // indicates visiting a map
|
|
h.hashMap(v, ti, doCheckCycles)
|
|
case reflect.String:
|
|
s := v.String()
|
|
h.hashUint64(uint64(len(s)))
|
|
w.WriteString(s)
|
|
case reflect.Bool:
|
|
if v.Bool() {
|
|
h.hashUint8(1)
|
|
} else {
|
|
h.hashUint8(0)
|
|
}
|
|
case reflect.Int8:
|
|
h.hashUint8(uint8(v.Int()))
|
|
case reflect.Int16:
|
|
h.hashUint16(uint16(v.Int()))
|
|
case reflect.Int32:
|
|
h.hashUint32(uint32(v.Int()))
|
|
case reflect.Int64, reflect.Int:
|
|
h.hashUint64(uint64(v.Int()))
|
|
case reflect.Uint8:
|
|
h.hashUint8(uint8(v.Uint()))
|
|
case reflect.Uint16:
|
|
h.hashUint16(uint16(v.Uint()))
|
|
case reflect.Uint32:
|
|
h.hashUint32(uint32(v.Uint()))
|
|
case reflect.Uint64, reflect.Uint, reflect.Uintptr:
|
|
h.hashUint64(uint64(v.Uint()))
|
|
case reflect.Float32:
|
|
h.hashUint32(math.Float32bits(float32(v.Float())))
|
|
case reflect.Float64:
|
|
h.hashUint64(math.Float64bits(float64(v.Float())))
|
|
case reflect.Complex64:
|
|
h.hashUint32(math.Float32bits(real(complex64(v.Complex()))))
|
|
h.hashUint32(math.Float32bits(imag(complex64(v.Complex()))))
|
|
case reflect.Complex128:
|
|
h.hashUint64(math.Float64bits(real(complex128(v.Complex()))))
|
|
h.hashUint64(math.Float64bits(imag(complex128(v.Complex()))))
|
|
}
|
|
}
|
|
|
|
type mapHasher struct {
|
|
h hasher
|
|
valKey, valElem valueCache // re-usable values for map iteration
|
|
iter reflect.MapIter // re-usable map iterator
|
|
}
|
|
|
|
var mapHasherPool = &sync.Pool{
|
|
New: func() any { return new(mapHasher) },
|
|
}
|
|
|
|
type valueCache map[reflect.Type]reflect.Value
|
|
|
|
func (c *valueCache) get(t reflect.Type) reflect.Value {
|
|
v, ok := (*c)[t]
|
|
if !ok {
|
|
v = reflect.New(t).Elem()
|
|
if *c == nil {
|
|
*c = make(valueCache)
|
|
}
|
|
(*c)[t] = v
|
|
}
|
|
return v
|
|
}
|
|
|
|
// hashMap hashes a map in a sort-free manner.
|
|
// It relies on a map being a functionally an unordered set of KV entries.
|
|
// So long as we hash each KV entry together, we can XOR all
|
|
// of the individual hashes to produce a unique hash for the entire map.
|
|
func (h *hasher) hashMap(v reflect.Value, ti *typeInfo, checkCycles bool) {
|
|
mh := mapHasherPool.Get().(*mapHasher)
|
|
defer mapHasherPool.Put(mh)
|
|
|
|
iter := &mh.iter
|
|
iter.Reset(v)
|
|
defer iter.Reset(reflect.Value{}) // avoid pinning v from mh.iter when we return
|
|
|
|
var sum Sum
|
|
if v.IsNil() {
|
|
sum.sum[0] = 1 // something non-zero
|
|
}
|
|
|
|
k := mh.valKey.get(v.Type().Key())
|
|
e := mh.valElem.get(v.Type().Elem())
|
|
mh.h.visitStack = h.visitStack // always use the parent's visit stack to avoid cycles
|
|
for iter.Next() {
|
|
k.SetIterKey(iter)
|
|
e.SetIterValue(iter)
|
|
mh.h.reset()
|
|
mh.h.hashValueWithType(k, ti.keyTypeInfo, checkCycles)
|
|
mh.h.hashValueWithType(e, ti.elemTypeInfo, checkCycles)
|
|
sum.xor(mh.h.sum())
|
|
}
|
|
h.bw.Write(append(h.scratch[:0], sum.sum[:]...)) // append into scratch to avoid heap allocation
|
|
}
|
|
|
|
// visitStack is a stack of pointers visited.
|
|
// Pointers are pushed onto the stack when visited, and popped when leaving.
|
|
// The integer value is the depth at which the pointer was visited.
|
|
// The length of this stack should be zero after every hashing operation.
|
|
type visitStack map[pointer]int
|
|
|
|
func (v visitStack) seen(p pointer) (int, bool) {
|
|
idx, ok := v[p]
|
|
return idx, ok
|
|
}
|
|
|
|
func (v *visitStack) push(p pointer) {
|
|
if *v == nil {
|
|
*v = make(map[pointer]int)
|
|
}
|
|
(*v)[p] = len(*v)
|
|
}
|
|
|
|
func (v visitStack) pop(p pointer) {
|
|
delete(v, p)
|
|
}
|
|
|
|
// pointer is a thin wrapper over unsafe.Pointer.
|
|
// We only rely on comparability of pointers; we cannot rely on uintptr since
|
|
// that would break if Go ever switched to a moving GC.
|
|
type pointer struct{ p unsafe.Pointer }
|
|
|
|
func pointerOf(v reflect.Value) pointer {
|
|
return pointer{unsafe.Pointer(v.Pointer())}
|
|
}
|
|
|
|
// hashType hashes a reflect.Type.
|
|
// The hash is only consistent within the lifetime of a program.
|
|
func (h *hasher) hashType(t reflect.Type) {
|
|
// This approach relies on reflect.Type always being backed by a unique
|
|
// *reflect.rtype pointer. A safer approach is to use a global sync.Map
|
|
// that maps reflect.Type to some arbitrary and unique index.
|
|
// While safer, it requires global state with memory that can never be GC'd.
|
|
rtypeAddr := reflect.ValueOf(t).Pointer() // address of *reflect.rtype
|
|
h.hashUint64(uint64(rtypeAddr))
|
|
}
|