From 2a22ea3e831a699e89fe9aa379ac0f3fcb177be9 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 14 Jun 2022 22:49:11 -0700 Subject: [PATCH] util/deephash: generate type-specific hasher funcs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- util/deephash/deephash.go | 434 ++++++++++++++++++++++++++++++++- util/deephash/deephash_test.go | 235 +++++++++++++++++- 2 files changed, 667 insertions(+), 2 deletions(-) diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index 4defffca1..82bb1a05e 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -26,6 +26,7 @@ "encoding/hex" "fmt" "hash" + "log" "math" "reflect" "sync" @@ -142,6 +143,35 @@ func Hash(v any) (s Sum) { 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) @@ -170,14 +200,26 @@ 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)) +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 @@ -190,11 +232,394 @@ type typeInfo struct { // 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) @@ -353,6 +778,13 @@ func (h *hasher) hashValueWithType(v reflect.Value, ti *typeInfo, forceCycleChec w := h.bw doCheckCycles := forceCycleChecking || ti.isRecursive + if !doCheckCycles { + hf := ti.hasher() + if hf(h, v) { + return + } + } + // Generic handling. switch v.Kind() { default: diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 643c42806..b30d69584 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -392,6 +392,238 @@ func TestCanMemHash(t *testing.T) { } } +func TestGetTypeHasher(t *testing.T) { + switch runtime.GOARCH { + case "amd64", "arm64", "arm", "386", "riscv64": + default: + // Test outputs below are specifically for little-endian machines. + // Just skip everything else for now. Feel free to add more above if + // you have the hardware to test and it's little-endian. + t.Skipf("skipping on %v", runtime.GOARCH) + } + type typedString string + var ( + someInt = int('A') + someComplex128 = complex128(1 + 2i) + someIP = netaddr.MustParseIP("1.2.3.4") + ) + tests := []struct { + name string + val any + want bool // set true automatically if out != "" + out string + out32 string // overwrites out if 32-bit + }{ + { + name: "int", + val: int(1), + out: "\x01\x00\x00\x00\x00\x00\x00\x00", + }, + { + name: "int_negative", + val: int(-1), + out: "\xff\xff\xff\xff\xff\xff\xff\xff", + }, + { + name: "int8", + val: int8(1), + out: "\x01", + }, + { + name: "float64", + val: float64(1.0), + out: "\x00\x00\x00\x00\x00\x00\xf0?", + }, + { + name: "float32", + val: float32(1.0), + out: "\x00\x00\x80?", + }, + { + name: "string", + val: "foo", + out: "\x03\x00\x00\x00\x00\x00\x00\x00foo", + }, + { + name: "typedString", + val: typedString("foo"), + out: "\x03\x00\x00\x00\x00\x00\x00\x00foo", + }, + { + name: "string_slice", + val: []string{"foo", "bar"}, + out: "\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x03\x00\x00\x00\x00\x00\x00\x00bar", + }, + { + name: "int_slice", + val: []int{1, 0, -1}, + out: "\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff", + out32: "\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff", + }, + { + name: "struct", + val: struct { + a, b int + c uint16 + }{1, -1, 2}, + out: "\x01\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x02\x00", + }, + { + name: "nil_int_ptr", + val: (*int)(nil), + out: "\x00", + }, + { + name: "int_ptr", + val: &someInt, + out: "\x01A\x00\x00\x00\x00\x00\x00\x00", + out32: "\x01A\x00\x00\x00", + }, + { + name: "nil_uint32_ptr", + val: (*uint32)(nil), + out: "\x00", + }, + { + name: "complex128_ptr", + val: &someComplex128, + out: "\x01\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@", + }, + { + name: "packet_filter", + val: filterRules, + out: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + out32: "\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00*\v\x00\x00\x00\x00\x00\x00\x0010.1.3.4/32\v\x00\x00\x00\x00\x00\x00\x0010.0.0.0/24\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01 \x00\x00\x00\x01\x00\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x001.2.3.4/32\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00foo\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00\x00\x00\x00\x00foooooooooo\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00\x00\x00\x00\x00baaaaaarrrrr\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }, + { + name: "netaddr.IP", + val: netaddr.MustParseIP("fe80::123%foo"), + out: "\r\x00\x00\x00\x00\x00\x00\x00fe80::123%foo", + }, + { + name: "ptr-netaddr.IP", + val: &someIP, + out: "\x01\a\x00\x00\x00\x00\x00\x00\x001.2.3.4", + }, + { + name: "ptr-nil-netaddr.IP", + val: (*netaddr.IP)(nil), + out: "\x00", + }, + { + name: "time", + val: time.Unix(0, 0).In(time.UTC), + out: "\x141970-01-01T00:00:00Z", + }, + { + name: "time_custom_zone", + val: time.Unix(1655311822, 0).In(time.FixedZone("FOO", -60*60)), + out: "\x192022-06-15T15:50:22-01:00", + }, + { + name: "time_nil", + val: (*time.Time)(nil), + out: "\x00", + }, + { + name: "array_memhash", + val: [4]byte{1, 2, 3, 4}, + out: "\x01\x02\x03\x04", + }, + { + name: "array_ptr_memhash", + val: ptrTo([4]byte{1, 2, 3, 4}), + out: "\x01\x01\x02\x03\x04", + }, + { + name: "ptr_to_struct_partially_memhashable", + val: &struct { + A int16 + B int16 + C *int + }{5, 6, nil}, + out: "\x01\x05\x00\x06\x00\x00", + }, + { + name: "struct_partially_memhashable_but_cant_addr", + val: struct { + A int16 + B int16 + C *int + }{5, 6, nil}, + out: "\x05\x00\x06\x00\x00", + }, + { + name: "array_elements", + val: [4]byte{1, 2, 3, 4}, + out: "\x01\x02\x03\x04", + }, + { + name: "bool", + val: true, + out: "\x01", + }, + { + name: "IntIntByteInt", + val: IntIntByteInt{1, 2, 3, 4}, + out: "\x01\x00\x00\x00\x02\x00\x00\x00\x03\x04\x00\x00\x00", + }, + { + name: "IntIntByteInt-canddr", + val: &IntIntByteInt{1, 2, 3, 4}, + out: "\x01\x01\x00\x00\x00\x02\x00\x00\x00\x03\x04\x00\x00\x00", + }, + { + name: "array-IntIntByteInt", + val: [2]IntIntByteInt{ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + }, + out: "\x01\x00\x00\x00\x02\x00\x00\x00\x03\x04\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\a\b\x00\x00\x00", + }, + { + name: "array-IntIntByteInt-canaddr", + val: &[2]IntIntByteInt{ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + }, + out: "\x01\x01\x00\x00\x00\x02\x00\x00\x00\x03\x04\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\a\b\x00\x00\x00", + }, + { + name: "tailcfg.Node", + val: &tailcfg.Node{}, + out: "\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x140001-01-01T00:00:00Z\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x140001-01-01T00:00:00Z\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rv := reflect.ValueOf(tt.val) + fn := getTypeInfo(rv.Type()).hasher() + var buf bytes.Buffer + h := &hasher{ + bw: bufio.NewWriter(&buf), + } + got := fn(h, rv) + const ptrSize = 32 << uintptr(^uintptr(0)>>63) + if tt.out32 != "" && ptrSize == 32 { + tt.out = tt.out32 + } + if tt.out != "" { + tt.want = true + } + if got != tt.want { + t.Fatalf("func returned %v; want %v", got, tt.want) + } + if err := h.bw.Flush(); err != nil { + t.Fatal(err) + } + if got := buf.String(); got != tt.out { + t.Fatalf("got %q; want %q", got, tt.out) + } + }) + } +} + var sink = Hash("foo") func BenchmarkHash(b *testing.B) { @@ -448,8 +680,9 @@ func ptrTo[T any](v T) *T { return &v } func BenchmarkHashPacketFilter(b *testing.B) { b.ReportAllocs() + hash := HasherForType[[]tailcfg.FilterRule]() for i := 0; i < b.N; i++ { - sink = Hash(filterRules) + sink = hash(filterRules) } }