Skip to content

Commit a26f860

Browse files
committed
runtime: use 32-bit hash for maps on Wasm
Currently we use 64-bit hash calculations on Wasm. The 64-bit hash calculation make intensive uses of 64x64->128 bit multiplications, which on many 64-bit platforms are compiler intrinsics and can be compiled to one or two instructions. This is not the case on Wasm, so it is not very performant. This CL makes it use 32-bit hashes on Wasm, just like other 32-bit architectures. The 32-bit hash calculation only uses 32x32->64 bit multiplications, which can be compiled efficiently on Wasm. Using 32-bit hashes may increase the chance of collisions. But it is the same as 32-bit architectures like 386. And our Wasm port supports only 32-bit address space (like 386), so this is not too bad. Runtime Hash benchmark results goos: js goarch: wasm pkg: runtime │ 0h.txt │ 1h.txt │ │ sec/op │ sec/op vs base │ Hash5 20.45n ± 9% 14.06n ± 2% -31.21% (p=0.000 n=10) Hash16 22.34n ± 7% 17.52n ± 1% -21.62% (p=0.000 n=10) Hash64 47.47n ± 3% 28.68n ± 1% -39.59% (p=0.000 n=10) Hash1024 475.4n ± 1% 271.4n ± 0% -42.92% (p=0.000 n=10) Hash65536 28.42µ ± 1% 16.66µ ± 0% -41.40% (p=0.000 n=10) HashStringSpeed 40.07n ± 7% 29.23n ± 1% -27.05% (p=0.000 n=10) HashBytesSpeed 62.01n ± 3% 46.11n ± 4% -25.64% (p=0.000 n=10) HashInt32Speed 24.31n ± 2% 20.39n ± 1% -16.13% (p=0.000 n=10) HashInt64Speed 25.48n ± 7% 20.81n ± 7% -18.29% (p=0.000 n=10) HashStringArraySpeed 87.69n ± 4% 76.65n ± 2% -12.58% (p=0.000 n=10) FastrandHashiter 87.65n ± 1% 87.65n ± 1% ~ (p=0.896 n=10) geomean 90.82n 67.03n -26.19% Map benchmarks are too many to post here. The speedups are around 0-40%. Change-Id: I2f7a68cfc446ab5a547fdb6a40aea07854516d51 Reviewed-on: https://go-review.googlesource.com/c/go/+/714600 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Michael Pratt <[email protected]>
1 parent 747fe2e commit a26f860

File tree

7 files changed

+38
-22
lines changed

7 files changed

+38
-22
lines changed

src/hash/maphash/maphash_runtime.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ package maphash
88

99
import (
1010
"internal/abi"
11-
"internal/goarch"
11+
"internal/runtime/maps"
1212
"unsafe"
1313
)
1414

@@ -29,10 +29,10 @@ func rthash(buf []byte, seed uint64) uint64 {
2929
// The runtime hasher only works on uintptr. For 64-bit
3030
// architectures, we use the hasher directly. Otherwise,
3131
// we use two parallel hashers on the lower and upper 32 bits.
32-
if goarch.PtrSize == 8 {
32+
if maps.Use64BitHash {
3333
return uint64(runtime_memhash(unsafe.Pointer(&buf[0]), uintptr(seed), uintptr(len)))
3434
}
35-
lo := runtime_memhash(unsafe.Pointer(&buf[0]), uintptr(seed), uintptr(len))
35+
lo := runtime_memhash(unsafe.Pointer(&buf[0]), uintptr(uint32(seed)), uintptr(len))
3636
hi := runtime_memhash(unsafe.Pointer(&buf[0]), uintptr(seed>>32), uintptr(len))
3737
return uint64(hi)<<32 | uint64(lo)
3838
}
@@ -51,10 +51,10 @@ func comparableHash[T comparable](v T, seed Seed) uint64 {
5151
var m map[T]struct{}
5252
mTyp := abi.TypeOf(m)
5353
hasher := (*abi.MapType)(unsafe.Pointer(mTyp)).Hasher
54-
if goarch.PtrSize == 8 {
54+
if maps.Use64BitHash {
5555
return uint64(hasher(abi.NoEscape(unsafe.Pointer(&v)), uintptr(s)))
5656
}
57-
lo := hasher(abi.NoEscape(unsafe.Pointer(&v)), uintptr(s))
57+
lo := hasher(abi.NoEscape(unsafe.Pointer(&v)), uintptr(uint32(s)))
5858
hi := hasher(abi.NoEscape(unsafe.Pointer(&v)), uintptr(s>>32))
5959
return uint64(hi)<<32 | uint64(lo)
6060
}

src/hash/maphash/smhasher_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ package maphash
88

99
import (
1010
"fmt"
11+
"internal/runtime/maps"
1112
"internal/testenv"
1213
"math"
1314
"math/rand"
1415
"runtime"
1516
"slices"
1617
"strings"
1718
"testing"
18-
"unsafe"
1919
)
2020

2121
// Smhasher is a torture test for hash functions.
@@ -486,7 +486,7 @@ func text(t *testing.T, h *hashSet, prefix, suffix string) {
486486

487487
// Make sure different seed values generate different hashes.
488488
func TestSmhasherSeed(t *testing.T) {
489-
if unsafe.Sizeof(uintptr(0)) == 4 {
489+
if !maps.Use64BitHash {
490490
t.Skip("32-bit platforms don't have ideal seed-input distributions (see issue 33988)")
491491
}
492492
t.Parallel()

src/internal/runtime/maps/map.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,12 @@ type Map struct {
245245
clearSeq uint64
246246
}
247247

248+
// Use 64-bit hash on 64-bit systems, except on Wasm, where we use
249+
// 32-bit hash (see runtime/hash32.go).
250+
const Use64BitHash = goarch.PtrSize == 8 && goarch.IsWasm == 0
251+
248252
func depthToShift(depth uint8) uint8 {
249-
if goarch.PtrSize == 4 {
253+
if !Use64BitHash {
250254
return 32 - depth
251255
}
252256
return 64 - depth

src/internal/runtime/maps/table.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package maps
66

77
import (
88
"internal/abi"
9-
"internal/goarch"
109
"internal/runtime/math"
1110
"unsafe"
1211
)
@@ -1170,7 +1169,7 @@ func (t *table) rehash(typ *abi.MapType, m *Map) {
11701169

11711170
// Bitmask for the last selection bit at this depth.
11721171
func localDepthMask(localDepth uint8) uintptr {
1173-
if goarch.PtrSize == 4 {
1172+
if !Use64BitHash {
11741173
return uintptr(1) << (32 - localDepth)
11751174
}
11761175
return uintptr(1) << (64 - localDepth)

src/runtime/alg.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ import (
1414
)
1515

1616
const (
17-
c0 = uintptr((8-goarch.PtrSize)/4*2860486313 + (goarch.PtrSize-4)/4*33054211828000289)
18-
c1 = uintptr((8-goarch.PtrSize)/4*3267000013 + (goarch.PtrSize-4)/4*23344194077549503)
17+
// We use 32-bit hash on Wasm, see hash32.go.
18+
hashSize = (1-goarch.IsWasm)*goarch.PtrSize + goarch.IsWasm*4
19+
c0 = uintptr((8-hashSize)/4*2860486313 + (hashSize-4)/4*33054211828000289)
20+
c1 = uintptr((8-hashSize)/4*3267000013 + (hashSize-4)/4*23344194077549503)
1921
)
2022

23+
func trimHash(h uintptr) uintptr {
24+
if goarch.IsWasm != 0 {
25+
// On Wasm, we use 32-bit hash, despite that uintptr is 64-bit.
26+
// memhash* always returns a uintptr with high 32-bit being 0
27+
// (see hash32.go). We trim the hash in other places where we
28+
// compute the hash manually, e.g. in interhash.
29+
return uintptr(uint32(h))
30+
}
31+
return h
32+
}
33+
2134
func memhash0(p unsafe.Pointer, h uintptr) uintptr {
2235
return h
2336
}
@@ -100,9 +113,9 @@ func f32hash(p unsafe.Pointer, h uintptr) uintptr {
100113
f := *(*float32)(p)
101114
switch {
102115
case f == 0:
103-
return c1 * (c0 ^ h) // +0, -0
116+
return trimHash(c1 * (c0 ^ h)) // +0, -0
104117
case f != f:
105-
return c1 * (c0 ^ h ^ uintptr(rand())) // any kind of NaN
118+
return trimHash(c1 * (c0 ^ h ^ uintptr(rand()))) // any kind of NaN
106119
default:
107120
return memhash(p, h, 4)
108121
}
@@ -112,9 +125,9 @@ func f64hash(p unsafe.Pointer, h uintptr) uintptr {
112125
f := *(*float64)(p)
113126
switch {
114127
case f == 0:
115-
return c1 * (c0 ^ h) // +0, -0
128+
return trimHash(c1 * (c0 ^ h)) // +0, -0
116129
case f != f:
117-
return c1 * (c0 ^ h ^ uintptr(rand())) // any kind of NaN
130+
return trimHash(c1 * (c0 ^ h ^ uintptr(rand()))) // any kind of NaN
118131
default:
119132
return memhash(p, h, 8)
120133
}
@@ -145,9 +158,9 @@ func interhash(p unsafe.Pointer, h uintptr) uintptr {
145158
panic(errorString("hash of unhashable type " + toRType(t).string()))
146159
}
147160
if t.IsDirectIface() {
148-
return c1 * typehash(t, unsafe.Pointer(&a.data), h^c0)
161+
return trimHash(c1 * typehash(t, unsafe.Pointer(&a.data), h^c0))
149162
} else {
150-
return c1 * typehash(t, a.data, h^c0)
163+
return trimHash(c1 * typehash(t, a.data, h^c0))
151164
}
152165
}
153166

@@ -172,9 +185,9 @@ func nilinterhash(p unsafe.Pointer, h uintptr) uintptr {
172185
panic(errorString("hash of unhashable type " + toRType(t).string()))
173186
}
174187
if t.IsDirectIface() {
175-
return c1 * typehash(t, unsafe.Pointer(&a.data), h^c0)
188+
return trimHash(c1 * typehash(t, unsafe.Pointer(&a.data), h^c0))
176189
} else {
177-
return c1 * typehash(t, a.data, h^c0)
190+
return trimHash(c1 * typehash(t, a.data, h^c0))
178191
}
179192
}
180193

src/runtime/hash32.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Hashing algorithm inspired by
66
// wyhash: https://github.com/wangyi-fudan/wyhash/blob/ceb019b530e2c1c14d70b79bfa2bc49de7d95bc1/Modern%20Non-Cryptographic%20Hash%20Function%20and%20Pseudorandom%20Number%20Generator.pdf
77

8-
//go:build 386 || arm || mips || mipsle
8+
//go:build 386 || arm || mips || mipsle || wasm
99

1010
package runtime
1111

src/runtime/hash64.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Hashing algorithm inspired by
66
// wyhash: https://github.com/wangyi-fudan/wyhash
77

8-
//go:build amd64 || arm64 || loong64 || mips64 || mips64le || ppc64 || ppc64le || riscv64 || s390x || wasm
8+
//go:build amd64 || arm64 || loong64 || mips64 || mips64le || ppc64 || ppc64le || riscv64 || s390x
99

1010
package runtime
1111

0 commit comments

Comments
 (0)