Skip to content

Commit

Permalink
hash/maphash: add WriteComparable and Comparable
Browse files Browse the repository at this point in the history
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 golang#54670

Change-Id: Ibd0538a7dfb3d831c5145970cac7c910692bca69
  • Loading branch information
qiulaidongfeng committed Aug 30, 2024
1 parent ffb3e57 commit 7fad797
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 0 deletions.
2 changes: 2 additions & 0 deletions api/next/54670.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions doc/next/6-stdlib/99-minor/hash/maphash/54670.md
Original file line number Diff line number Diff line change
@@ -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].
26 changes: 26 additions & 0 deletions src/hash/maphash/maphash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 [8]byte
byteorder.LeAppendUint64(buf[:], v)
h.Write(buf[:])
}
61 changes: 61 additions & 0 deletions src/hash/maphash/maphash_purego.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ package maphash

import (
"crypto/rand"
"internal/abi"
"internal/byteorder"
"math/bits"
"reflect"
)

func rthash(buf []byte, seed uint64) uint64 {
Expand Down Expand Up @@ -92,3 +94,62 @@ 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())
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))
}
case reflect.String:
return append(buf, v.String()...)
case reflect.Struct:
for i := range v.NumField() {
buf = appendT(buf, v.Field(i))
}
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
}
22 changes: 22 additions & 0 deletions src/hash/maphash/maphash_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package maphash

import (
"internal/abi"
"internal/unsafeheader"
"unsafe"
)

Expand Down Expand Up @@ -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)
}
47 changes: 47 additions & 0 deletions src/hash/maphash/maphash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down

0 comments on commit 7fad797

Please sign in to comment.