Skip to content

Commit 7fad797

Browse files
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 golang#54670 Change-Id: Ibd0538a7dfb3d831c5145970cac7c910692bca69
1 parent ffb3e57 commit 7fad797

File tree

6 files changed

+160
-0
lines changed

6 files changed

+160
-0
lines changed

api/next/54670.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pkg hash/maphash, func Comparable[$0 comparable](Seed, $0) uint64 #54670
2+
pkg hash/maphash, func WriteComparable[$0 comparable](*Hash, $0) #54670
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
New function [Comparable] returns the hash of comparable value v.
2+
New function [WriteComparable] adds x to the data hashed by [Hash].

src/hash/maphash/maphash.go

+26
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
// (See crypto/sha256 and crypto/sha512 for cryptographic use.)
1313
package maphash
1414

15+
import (
16+
"internal/abi"
17+
"internal/byteorder"
18+
)
19+
1520
// A Seed is a random value that selects the specific hash function
1621
// computed by a [Hash]. If two Hashes use the same Seeds, they
1722
// will compute the same hash values for any given input.
@@ -275,3 +280,24 @@ func (h *Hash) Size() int { return 8 }
275280

276281
// BlockSize returns h's block size.
277282
func (h *Hash) BlockSize() int { return len(h.buf) }
283+
284+
// Comparable returns the hash of comparable value v with the given seed
285+
// such that Comparable(s, v1) == Comparable(s, v2) if v1 == v2.
286+
// If v contains a floating-point NaN, then the hash is non-deterministically random.
287+
func Comparable[T comparable](seed Seed, v T) uint64 {
288+
abi.Escape(v)
289+
t := abi.TypeOf(v)
290+
len := t.Size()
291+
if len == 0 {
292+
return seed.s
293+
}
294+
return comparableF(seed.s, v, t)
295+
}
296+
297+
// WriteComparable adds x to the data hashed by h.
298+
func WriteComparable[T comparable](h *Hash, x T) {
299+
v := Comparable(h.seed, x)
300+
var buf [8]byte
301+
byteorder.LeAppendUint64(buf[:], v)
302+
h.Write(buf[:])
303+
}

src/hash/maphash/maphash_purego.go

+61
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package maphash
88

99
import (
1010
"crypto/rand"
11+
"internal/abi"
1112
"internal/byteorder"
1213
"math/bits"
14+
"reflect"
1315
)
1416

1517
func rthash(buf []byte, seed uint64) uint64 {
@@ -92,3 +94,62 @@ func mix(a, b uint64) uint64 {
9294
hi, lo := bits.Mul64(a, b)
9395
return hi ^ lo
9496
}
97+
98+
var byteSliceTyp = reflect.TypeFor[[]byte]()
99+
100+
var strSliceTyp = reflect.TypeFor[string]()
101+
102+
func comparableF[T comparable](seed uint64, v T, t *abi.Type) uint64 {
103+
vv := reflect.ValueOf(v)
104+
typ := vv.Type()
105+
if typ == byteSliceTyp {
106+
return wyhash(vv.Bytes(), seed, uint64(vv.Len()))
107+
}
108+
if typ == strSliceTyp {
109+
return rthashString(vv.String(), seed)
110+
}
111+
buf := make([]byte, 0, typ.Size())
112+
appendT(buf, vv)
113+
return wyhash(buf, seed, uint64(len(buf)))
114+
}
115+
116+
func appendT(buf []byte, v reflect.Value) []byte {
117+
switch v.Kind() {
118+
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
119+
return byteorder.LeAppendUint64(buf, uint64(v.Int()))
120+
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr:
121+
return byteorder.LeAppendUint64(buf, v.Uint())
122+
case reflect.Array, reflect.Slice:
123+
for i := range v.Len() {
124+
buf = appendT(buf, v.Index(i))
125+
}
126+
case reflect.String:
127+
return append(buf, v.String()...)
128+
case reflect.Struct:
129+
for i := range v.NumField() {
130+
buf = appendT(buf, v.Field(i))
131+
}
132+
case reflect.Complex64, reflect.Complex128:
133+
c := v.Complex()
134+
buf = byteorder.LeAppendUint64(buf, uint64(real(c)))
135+
return byteorder.LeAppendUint64(buf, uint64(imag(c)))
136+
case reflect.Float32, reflect.Float64:
137+
return byteorder.LeAppendUint64(buf, uint64(v.Float()))
138+
case reflect.Bool:
139+
return byteorder.LeAppendUint16(buf, btoi(v.Bool()))
140+
case reflect.UnsafePointer, reflect.Pointer:
141+
return byteorder.LeAppendUint64(buf, uint64(v.Pointer()))
142+
case reflect.Interface:
143+
a := v.InterfaceData()
144+
buf = byteorder.LeAppendUint64(buf, uint64(a[0]))
145+
return byteorder.LeAppendUint64(buf, uint64(a[1]))
146+
}
147+
panic("unreachable")
148+
}
149+
150+
func btoi(b bool) uint16 {
151+
if b {
152+
return 1
153+
}
154+
return 0
155+
}

src/hash/maphash/maphash_runtime.go

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
package maphash
88

99
import (
10+
"internal/abi"
11+
"internal/unsafeheader"
1012
"unsafe"
1113
)
1214

@@ -41,3 +43,23 @@ func rthashString(s string, state uint64) uint64 {
4143
func randUint64() uint64 {
4244
return runtime_rand()
4345
}
46+
47+
func comparableF[T comparable](seed uint64, v T, t *abi.Type) uint64 {
48+
k := t.Kind()
49+
len := t.Size()
50+
ptr := unsafe.Pointer(&v)
51+
switch k {
52+
case abi.Slice:
53+
len = uintptr(((*unsafeheader.Slice)(unsafe.Pointer(&v))).Len) * t.Elem().Size()
54+
ptr = ((*unsafeheader.Slice)(unsafe.Pointer(&v))).Data
55+
case abi.String:
56+
len = uintptr(((*unsafeheader.String)(unsafe.Pointer(&v))).Len)
57+
ptr = ((*unsafeheader.String)(unsafe.Pointer(&v))).Data
58+
}
59+
if unsafe.Sizeof(uintptr(0)) == 8 {
60+
return uint64(runtime_memhash(ptr, uintptr(seed), len))
61+
}
62+
lo := runtime_memhash(ptr, uintptr(seed), len)
63+
hi := runtime_memhash(ptr, uintptr(seed>>32), len)
64+
return uint64(hi)<<32 | uint64(lo)
65+
}

src/hash/maphash/maphash_test.go

+47
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,53 @@ func TestSeedFromReset(t *testing.T) {
210210
}
211211
}
212212

213+
func TestCompare(t *testing.T) {
214+
var a, b int = 2, 2
215+
var pa *int = &a
216+
seed := MakeSeed()
217+
if Comparable(seed, a) != Comparable(seed, b) {
218+
t.Fatal("Comparable(seed, 2) != Comparable(seed, 2)")
219+
}
220+
old := Comparable(seed, pa)
221+
stackGrow(8192)
222+
new := Comparable(seed, pa)
223+
if old != new {
224+
t.Fatal("Comparable(seed, ptr) != Comparable(seed, ptr)")
225+
}
226+
}
227+
228+
//go:noinline
229+
func stackGrow(dep int) {
230+
if dep == 0 {
231+
return
232+
}
233+
var local [1024]byte
234+
_ = local
235+
stackGrow(dep - 1)
236+
}
237+
238+
func TestWriteComparable(t *testing.T) {
239+
var a, b int = 2, 2
240+
var pa *int = &a
241+
h1 := Hash{}
242+
h2 := Hash{}
243+
h1.seed = MakeSeed()
244+
h2.seed = h1.seed
245+
WriteComparable(&h1, a)
246+
WriteComparable(&h2, b)
247+
if h1.Sum64() != h1.Sum64() {
248+
t.Fatal("WriteComparable(h, 2) != WriteComparable(h, 2)")
249+
}
250+
WriteComparable(&h1, pa)
251+
old := h1.Sum64()
252+
stackGrow(8192)
253+
WriteComparable(&h2, pa)
254+
new := h2.Sum64()
255+
if old != new {
256+
t.Fatal("WriteComparable(seed, ptr) != WriteComparable(seed, ptr)")
257+
}
258+
}
259+
213260
// Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces.
214261
var _ hash.Hash = &Hash{}
215262
var _ hash.Hash64 = &Hash{}

0 commit comments

Comments
 (0)