hash/maphash: add WriteComparable and Comparable

By default, runtime.memhash is used.
When purego is used, reflect is used to generate the same []byte
with the same value, and then hash the []byte.

Fixes #54670

Change-Id: Ibd0538a7dfb3d831c5145970cac7c910692bca69
This commit is contained in:
qiulaidongfeng 2024-08-30 19:23:47 +08:00
parent ffb3e57401
commit 02df413842
6 changed files with 162 additions and 0 deletions

2
api/next/54670.txt Normal file
View File

@ -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

View File

@ -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].

View File

@ -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[:])
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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{}