Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/internal/runtime/maps/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const MaxAvgGroupLoad = maxAvgGroupLoad
// we can't properly test hint alloc overflows with this.
const maxAllocTest = 1 << 30

// Do not convert to var-of-funcval, these are used in benchmarks and must be
// inlined.
func MemHashAES(p unsafe.Pointer, h, s uintptr) uintptr { return memHashAES(p, h, s) }
func MemHashFallback(p unsafe.Pointer, h, s uintptr) uintptr { return memHashFallback(p, h, s) }

func newTestMapType[K comparable, V any]() *abi.MapType {
var m map[K]V
mTyp := abi.TypeOf(m)
Expand Down
6 changes: 3 additions & 3 deletions src/internal/runtime/maps/memhash_aes.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ import (
const memHashAESImplemented = true

func MemHash(p unsafe.Pointer, h, s uintptr) uintptr {
if UseAeshash {
if s >= MinAeshashSize {
return memHashAES(p, h, s)
}
return memHashFallback(p, h, s)
}

func MemHash32(k uint32, h uintptr) uintptr {
if UseAeshash {
if useAeshash32 {
return memHash32AES(k, h)
}
return memHash32Fallback(k, h)
}

func MemHash64(k uint64, h uintptr) uintptr {
if UseAeshash {
if useAeshash64 {
return memHash64AES(k, h)
}
return memHash64Fallback(k, h)
Expand Down
91 changes: 91 additions & 0 deletions src/internal/runtime/maps/memhash_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build amd64 || arm64

package maps_test

import (
"fmt"
"testing"
"unsafe"

"internal/runtime/maps"
)

var sink uintptr

// BenchmarkHashBakeoff measures the AES and scalar memory hashers at
// various sizes to try to empirically determine when one becomes better than
// the other, for some target uarch. Results are very uarch-dependent!
//
// The datapoints should be compared something like benchstat which uses the
// appropriate statistical tests to knock out outliers.
//
// Latency (i.e., serial pipeline performance) matters for probing, because
// there is a data dependency between the hash and the probe sequence. We can
// measure this by making each iteration of the benchmark depend on the previous
// one. This tends to favor scalar-only hashing more.
//
// Throughput (i.e., how long matters when many independent things are being
// hashed, resulting in better IPC. We measure this by using a seed of 0 for
// each iteration. This tends to favor AES more.
//
// Conservatively, we treat throughput as more important. However, more study
// is needed to determine if prioritizing latency (and thus picking a higher
// cutoff, such as MinLen = 112 on Zen4) results in better macrobenchmarks.
func BenchmarkHashBakeoff(b *testing.B) {
if !maps.AeshashEnabled() {
b.Skip("AES hashing not available on this machine")
}

buf := make([]byte, 1024+8)
for i := range buf {
buf[i] = byte(i * 63)
}
p := unsafe.Pointer(unsafe.SliceData(buf))

var sizes = []uintptr{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 100, 104, 108, 112, 116,
120, 124, 128, 192, 256, 512, 1024,
}

for _, s := range sizes {
b.Run(fmt.Sprintf("scalar/latency/%d", s), func(b *testing.B) {
var h uintptr
for b.Loop() {
h = maps.MemHashFallback(p, h, s)
}
sink = h
})
}
for _, s := range sizes {
b.Run(fmt.Sprintf("scalar/throughput/%d", s), func(b *testing.B) {
var h uintptr
for b.Loop() {
h ^= maps.MemHashFallback(p, 0, s)
}
sink = h
})
}
for _, s := range sizes {
b.Run(fmt.Sprintf("aes/latency/%d", s), func(b *testing.B) {
var h uintptr
for b.Loop() {
h = maps.MemHashAES(p, h, s)
}
sink = h
})
}
for _, s := range sizes {
b.Run(fmt.Sprintf("aes/throughput/%d", s), func(b *testing.B) {
var h uintptr
for b.Loop() {
h ^= maps.MemHashAES(p, 0, s)
}
sink = h
})
}
}
55 changes: 44 additions & 11 deletions src/internal/runtime/maps/runtime_alg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,27 @@ import (
"unsafe"
)

// runtime variable to check if the processor we're running on
// actually supports the instructions used by the AES-based
// hash implementation.
var UseAeshash bool
// MinAeshashSize is the smallest key size hashed with the AES-based
// implementation. Selecting hash is a size comparison against this value.
// Setting this to MaxUintptr disables AES altogether.
//
// When support is detected, the threshold is lowered to select between the
// scalar-based fallback hash and the vector-based AES hash. This value is
// selected on a per-platform basis based on what value produces the best
// benchmark results.
//
// Scalar hashes are faster on small values because it avoids taking a trip
// into the vector unit, which hurts latency (and for very small values,
// throughput).
var MinAeshashSize uintptr = ^uintptr(0)

// AeshashEnabled reports whether this machine hashes any sizes with AES.
//
// Test-only; compare against MinAeshashSize in non-test code to fuse this
// comparison with the MinAeshashSize check.
func AeshashEnabled() bool {
return MinAeshashSize != ^uintptr(0)
}

const hashRandomBytes = goarch.PtrSize / 4 * 64

Expand All @@ -24,6 +41,13 @@ var aeskeysched [hashRandomBytes]byte
// used in hash{32,64}.go to seed the hash function
var hashkey [4]uintptr

// Pre-computed comparisons against MinAeshashSize, which reduces a
// load-and-compare-and-branch to a load-and-branch.
var (
useAeshash32 bool // = MinAeshashSize <= 4
useAeshash64 bool // = MinAeshashSize <= 8
)

func AlgInit() {
// Always intialize hashkey.
//
Expand All @@ -46,23 +70,32 @@ func AlgInit() {
}
initAlgAES()

if memHashUsesVAES {
if memHashUsesVAES && !cpu.X86.HasAVX {
// We are using intrinsics hash implementation.
// Override the UseAeshash in this case, since it uses VAES (AVX) instructions.
// While assembly implementation used AES-NI instructions,
// simd intrinsics only provide access to AVX ones.
UseAeshash = cpu.X86.HasAVX
MinAeshashSize = ^uintptr(0)
}
return
}
if goarch.GOARCH == "arm64" && cpu.ARM64.HasAES {
} else if goarch.GOARCH == "arm64" && cpu.ARM64.HasAES {
initAlgAES()
return
}

useAeshash32 = 4 >= MinAeshashSize
useAeshash64 = 8 >= MinAeshashSize
}

func initAlgAES() {
UseAeshash = true
// TODO(mcy): investigate cutoffs on a per-uarch basis.
// See memhash_bench_test.go.
switch goarch.ArchFamily {
case goarch.AMD64:
// Measured on AMD Ryzen Threadripper PRO 7995WX (Zen4).
MinAeshashSize = 9
default:
MinAeshashSize = 0
}

// Initialize with random data so hash collisions will be hard to engineer.
key := (*[hashRandomBytes / 8]uint64)(unsafe.Pointer(&aeskeysched))
for i := range key {
Expand Down
8 changes: 4 additions & 4 deletions src/internal/runtime/maps/runtime_fast32.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ func runtime_mapaccess2_fast32(typ *abi.MapType, m *Map, key uint32) (unsafe.Poi
// But when we are using intrinsic implementation we want it to be inlined,
// since it improves performance.
//
// Note: memHashAESImplemented is compile time constant. We use it to remove runtime UseAeshash check
// Note: memHashAESImplemented is compile time constant. We use it to remove the useAeshash32 check
// for architectures where we don't have AES hashing implementations.
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash32 {
hash = memHash32AES(key, m.seed)
} else {
hash = memHash32Fallback(key, m.seed)
Expand Down Expand Up @@ -199,7 +199,7 @@ func runtime_mapassign_fast32(typ *abi.MapType, m *Map, key uint32) unsafe.Point

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash32 {
hash = memHash32AES(key, m.seed)
} else {
hash = memHash32Fallback(key, m.seed)
Expand Down Expand Up @@ -348,7 +348,7 @@ func runtime_mapassign_fast32ptr(typ *abi.MapType, m *Map, key unsafe.Pointer) u

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash32 {
hash = memHash32AES(uint32((uintptr)(key)), m.seed)
} else {
hash = memHash32Fallback(uint32((uintptr)(key)), m.seed)
Expand Down
6 changes: 3 additions & 3 deletions src/internal/runtime/maps/runtime_fast64.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func runtime_mapaccess2_fast64(typ *abi.MapType, m *Map, key uint64) (unsafe.Poi

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash64 {
hash = memHash64AES(key, m.seed)
} else {
hash = memHash64Fallback(key, m.seed)
Expand Down Expand Up @@ -194,7 +194,7 @@ func runtime_mapassign_fast64(typ *abi.MapType, m *Map, key uint64) unsafe.Point

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash64 {
hash = memHash64AES(key, m.seed)
} else {
hash = memHash64Fallback(key, m.seed)
Expand Down Expand Up @@ -412,7 +412,7 @@ func runtime_mapassign_fast64ptr(typ *abi.MapType, m *Map, key unsafe.Pointer) u

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && useAeshash64 {
hash = memHash64AES(uint64((uintptr)(key)), m.seed)
} else {
hash = memHash64Fallback(uint64((uintptr)(key)), m.seed)
Expand Down
6 changes: 3 additions & 3 deletions src/internal/runtime/maps/runtime_faststr.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ dohash:
// This path will cost 1 hash and 1+ε comparisons.
var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && uintptr(len(key)) >= MinAeshashSize {
hash = memHashAES(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
} else {
hash = memHashFallback(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
Expand Down Expand Up @@ -143,7 +143,7 @@ func runtime_mapaccess2_faststr(typ *abi.MapType, m *Map, key string) (unsafe.Po

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && uintptr(len(key)) >= MinAeshashSize {
hash = memHashAES(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
} else {
hash = memHashFallback(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
Expand Down Expand Up @@ -273,7 +273,7 @@ func runtime_mapassign_faststr(typ *abi.MapType, m *Map, key string) unsafe.Poin

var hash uintptr
// See the related comment in runtime_mapaccess2_fast32
if memHashAESImplemented && UseAeshash {
if memHashAESImplemented && uintptr(len(key)) >= MinAeshashSize {
hash = memHashAES(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
} else {
hash = memHashFallback(unsafe.Pointer(unsafe.StringData(key)), m.seed, uintptr(len(key)))
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ var (
IfaceHash = ifaceHash
)

var UseAeshash = &maps.UseAeshash
var MinAeshashSize = &maps.MinAeshashSize

var AeshashEnabled = maps.AeshashEnabled

func MemclrBytes(b []byte) {
s := (*slice)(unsafe.Pointer(&b))
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestMemHash32AlignAccess(t *testing.T) {
}

func TestMemHash32Equality(t *testing.T) {
if *UseAeshash {
if *MinAeshashSize <= 4 {
t.Skip("skipping since AES hash implementation is used")
}
var b [4]byte
Expand Down Expand Up @@ -60,7 +60,7 @@ func TestMemHash64AlignAccess(t *testing.T) {
}

func TestMemHash64Equality(t *testing.T) {
if *UseAeshash {
if *MinAeshashSize <= 8 {
t.Skip("skipping since AES hash implementation is used")
}
var b [8]byte
Expand Down Expand Up @@ -658,7 +658,7 @@ func TestSmhasherSeed(t *testing.T) {
}

func TestIssue66841(t *testing.T) {
if *UseAeshash && os.Getenv("TEST_ISSUE_66841") == "" {
if AeshashEnabled() && os.Getenv("TEST_ISSUE_66841") == "" {
// We want to test the backup hash, so if we're running on a machine
// that uses aeshash, exec ourselves while turning aes off.
cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.Executable(t), "-test.run=^TestIssue66841$"))
Expand Down
Loading