diff --git a/api/next/54670.txt b/api/next/54670.txt new file mode 100644 index 0000000000..d639a68d93 --- /dev/null +++ b/api/next/54670.txt @@ -0,0 +1,2 @@ +pkg hash/maphash, func Comparable[$0 comparable](Seed, $0) uint64 #54670 +pkg hash/maphash, func WriteComparable[$0 comparable](*Hash, $0) #54670 diff --git a/doc/next/6-stdlib/99-minor/hash/maphash/54670.md b/doc/next/6-stdlib/99-minor/hash/maphash/54670.md new file mode 100644 index 0000000000..ed67a4cb1f --- /dev/null +++ b/doc/next/6-stdlib/99-minor/hash/maphash/54670.md @@ -0,0 +1,2 @@ +New function [Comparable] returns the hash of comparable value v. +New function [WriteComparable] adds x to the data hashed by [Hash]. diff --git a/src/hash/maphash/maphash.go b/src/hash/maphash/maphash.go index 1e70a279f5..d39678c0fb 100644 --- a/src/hash/maphash/maphash.go +++ b/src/hash/maphash/maphash.go @@ -12,6 +12,11 @@ // (See crypto/sha256 and crypto/sha512 for cryptographic use.) package maphash +import ( + "internal/abi" + "internal/byteorder" +) + // A Seed is a random value that selects the specific hash function // computed by a [Hash]. If two Hashes use the same Seeds, they // will compute the same hash values for any given input. @@ -275,3 +280,24 @@ func (h *Hash) Size() int { return 8 } // BlockSize returns h's block size. func (h *Hash) BlockSize() int { return len(h.buf) } + +// Comparable returns the hash of comparable value v with the given seed +// such that Comparable(s, v1) == Comparable(s, v2) if v1 == v2. +// If v contains a floating-point NaN, then the hash is non-deterministically random. +func Comparable[T comparable](seed Seed, v T) uint64 { + abi.Escape(v) + t := abi.TypeOf(v) + len := t.Size() + if len == 0 { + return seed.s + } + return comparableF(seed.s, v, t) +} + +// WriteComparable adds x to the data hashed by h. +func WriteComparable[T comparable](h *Hash, x T) { + v := Comparable(h.seed, x) + var buf []byte + byteorder.LePutUint64(buf[:], v) + h.Write(buf[:]) +} diff --git a/src/hash/maphash/maphash_purego.go b/src/hash/maphash/maphash_purego.go index 38ac8c4df3..8d059835f2 100644 --- a/src/hash/maphash/maphash_purego.go +++ b/src/hash/maphash/maphash_purego.go @@ -8,8 +8,10 @@ package maphash import ( "crypto/rand" + "internal/abi" "internal/byteorder" "math/bits" + "reflect" ) func rthash(buf []byte, seed uint64) uint64 { @@ -92,3 +94,64 @@ func mix(a, b uint64) uint64 { hi, lo := bits.Mul64(a, b) return hi ^ lo } + +var byteSliceTyp = reflect.TypeFor[[]byte]() + +var strSliceTyp = reflect.TypeFor[string]() + +func comparableF[T comparable](seed uint64, v T, t *abi.Type) uint64 { + vv := reflect.ValueOf(v) + typ := vv.Type() + if typ == byteSliceTyp { + return wyhash(vv.Bytes(), seed, uint64(vv.Len())) + } + if typ == strSliceTyp { + return rthashString(vv.String(), seed) + } + buf := make([]byte, 0, typ.Size()) + buf = appendT(buf, vv) + return wyhash(buf, seed, uint64(len(buf))) +} + +func appendT(buf []byte, v reflect.Value) []byte { + switch v.Kind() { + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return byteorder.LeAppendUint64(buf, uint64(v.Int())) + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr: + return byteorder.LeAppendUint64(buf, v.Uint()) + case reflect.Array, reflect.Slice: + for i := range v.Len() { + buf = appendT(buf, v.Index(i)) + } + return buf + case reflect.String: + return append(buf, v.String()...) + case reflect.Struct: + for i := range v.NumField() { + buf = appendT(buf, v.Field(i)) + } + return buf + case reflect.Complex64, reflect.Complex128: + c := v.Complex() + buf = byteorder.LeAppendUint64(buf, uint64(real(c))) + return byteorder.LeAppendUint64(buf, uint64(imag(c))) + case reflect.Float32, reflect.Float64: + return byteorder.LeAppendUint64(buf, uint64(v.Float())) + case reflect.Bool: + return byteorder.LeAppendUint16(buf, btoi(v.Bool())) + case reflect.UnsafePointer, reflect.Pointer: + return byteorder.LeAppendUint64(buf, uint64(v.Pointer())) + case reflect.Interface: + a := v.InterfaceData() + buf = byteorder.LeAppendUint64(buf, uint64(a[0])) + return byteorder.LeAppendUint64(buf, uint64(a[1])) + } + panic("unreachable") +} + +func btoi(b bool) uint16 { + if b { + return 1 + } + return 0 +} diff --git a/src/hash/maphash/maphash_runtime.go b/src/hash/maphash/maphash_runtime.go index b831df2cf4..1e519acdba 100644 --- a/src/hash/maphash/maphash_runtime.go +++ b/src/hash/maphash/maphash_runtime.go @@ -7,6 +7,8 @@ package maphash import ( + "internal/abi" + "internal/unsafeheader" "unsafe" ) @@ -41,3 +43,23 @@ func rthashString(s string, state uint64) uint64 { func randUint64() uint64 { return runtime_rand() } + +func comparableF[T comparable](seed uint64, v T, t *abi.Type) uint64 { + k := t.Kind() + len := t.Size() + ptr := unsafe.Pointer(&v) + switch k { + case abi.Slice: + len = uintptr(((*unsafeheader.Slice)(unsafe.Pointer(&v))).Len) * t.Elem().Size() + ptr = ((*unsafeheader.Slice)(unsafe.Pointer(&v))).Data + case abi.String: + len = uintptr(((*unsafeheader.String)(unsafe.Pointer(&v))).Len) + ptr = ((*unsafeheader.String)(unsafe.Pointer(&v))).Data + } + if unsafe.Sizeof(uintptr(0)) == 8 { + return uint64(runtime_memhash(ptr, uintptr(seed), len)) + } + lo := runtime_memhash(ptr, uintptr(seed), len) + hi := runtime_memhash(ptr, uintptr(seed>>32), len) + return uint64(hi)<<32 | uint64(lo) +} diff --git a/src/hash/maphash/maphash_test.go b/src/hash/maphash/maphash_test.go index ed70f0ce51..214f968650 100644 --- a/src/hash/maphash/maphash_test.go +++ b/src/hash/maphash/maphash_test.go @@ -210,6 +210,53 @@ func TestSeedFromReset(t *testing.T) { } } +func TestCompare(t *testing.T) { + var a, b int = 2, 2 + var pa *int = &a + seed := MakeSeed() + if Comparable(seed, a) != Comparable(seed, b) { + t.Fatal("Comparable(seed, 2) != Comparable(seed, 2)") + } + old := Comparable(seed, pa) + stackGrow(8192) + new := Comparable(seed, pa) + if old != new { + t.Fatal("Comparable(seed, ptr) != Comparable(seed, ptr)") + } +} + +//go:noinline +func stackGrow(dep int) { + if dep == 0 { + return + } + var local [1024]byte + _ = local + stackGrow(dep - 1) +} + +func TestWriteComparable(t *testing.T) { + var a, b int = 2, 2 + var pa *int = &a + h1 := Hash{} + h2 := Hash{} + h1.seed = MakeSeed() + h2.seed = h1.seed + WriteComparable(&h1, a) + WriteComparable(&h2, b) + if h1.Sum64() != h1.Sum64() { + t.Fatal("WriteComparable(h, 2) != WriteComparable(h, 2)") + } + WriteComparable(&h1, pa) + old := h1.Sum64() + stackGrow(8192) + WriteComparable(&h2, pa) + new := h2.Sum64() + if old != new { + t.Fatal("WriteComparable(seed, ptr) != WriteComparable(seed, ptr)") + } +} + // Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces. var _ hash.Hash = &Hash{} var _ hash.Hash64 = &Hash{}