internal/runtime/maps/: devirtualize hashing for specialized maps

This change improves performance of specialized maps and opens opportunity for further improvement by inlining hashing calls in the future (right now we can't inline functions from GOASM).

MapAccessBenchmarks
goos: linux
goarch: amd64
pkg: runtime
cpu: Intel(R) Xeon(R) CPU E5-2690 v3 @ 2.60GHz
                                                 │ base-randlayout-meged.stat │    devirt-randlayout-merged.stat    │
                                                 │           sec/op           │   sec/op     vs base                │
MapAccessHit/Key=int32/Elem=int32/len=6-4                         21.35n ± 1%   21.72n ± 0%   +1.73% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=int32/len=64-4                        25.74n ± 1%   25.37n ± 2%   -1.44% (p=0.006 n=45)
MapAccessHit/Key=int32/Elem=int32/len=65536-4                     41.56n ± 0%   41.07n ± 0%   -1.18% (p=0.000 n=45)
MapAccessHit/Key=int64/Elem=int64/len=6-4                         21.53n ± 0%   21.56n ± 0%   +0.14% (p=0.000 n=45)
MapAccessHit/Key=int64/Elem=int64/len=64-4                        25.56n ± 0%   25.32n ± 2%        ~ (p=0.651 n=45)
MapAccessHit/Key=int64/Elem=int64/len=65536-4                     44.47n ± 0%   44.42n ± 0%        ~ (p=0.645 n=45)
MapAccessHit/Key=string/Elem=string/len=6-4                       30.54n ± 0%   30.83n ± 0%   +0.95% (p=0.000 n=45)
MapAccessHit/Key=string/Elem=string/len=64-4                      33.94n ± 0%   32.58n ± 0%   -4.01% (p=0.000 n=45)
MapAccessHit/Key=string/Elem=string/len=65536-4                   60.75n ± 1%   59.22n ± 0%   -2.52% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=bigType/len=6-4                       104.7n ± 0%   104.2n ± 0%   -0.48% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=bigType/len=64-4                      176.9n ± 2%   169.8n ± 1%   -4.01% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=bigType/len=65536-4                   603.3n ± 0%   604.6n ± 0%   +0.22% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=*int32/len=6-4                        21.75n ± 1%   21.90n ± 0%   +0.69% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=*int32/len=64-4                       26.28n ± 0%   25.74n ± 0%   -2.05% (p=0.000 n=45)
MapAccessHit/Key=int32/Elem=*int32/len=65536-4                    45.42n ± 0%   45.10n ± 0%   -0.70% (p=0.000 n=45)
MapAccessMiss/Key=int32/Elem=int32/len=6-4                        23.49n ± 0%   23.56n ± 1%   +0.30% (p=0.000 n=45)
MapAccessMiss/Key=int32/Elem=int32/len=64-4                       24.93n ± 3%   24.85n ± 2%        ~ (p=0.144 n=45)
MapAccessMiss/Key=int32/Elem=int32/len=65536-4                    37.68n ± 0%   37.20n ± 0%   -1.27% (p=0.000 n=45)
MapAccessMiss/Key=int64/Elem=int64/len=6-4                        23.49n ± 0%   23.44n ± 1%        ~ (p=0.587 n=45)
MapAccessMiss/Key=int64/Elem=int64/len=64-4                       25.66n ± 2%   25.02n ± 2%   -2.49% (p=0.032 n=45)
MapAccessMiss/Key=int64/Elem=int64/len=65536-4                    38.19n ± 0%   37.83n ± 0%   -0.94% (p=0.000 n=45)
MapAccessMiss/Key=string/Elem=string/len=6-4                      31.29n ± 0%   31.55n ± 0%   +0.83% (p=0.000 n=45)
MapAccessMiss/Key=string/Elem=string/len=64-4                     31.28n ± 3%   30.81n ± 2%   -1.50% (p=0.025 n=45)
MapAccessMiss/Key=string/Elem=string/len=65536-4                  46.76n ± 0%   45.81n ± 0%   -2.03% (p=0.000 n=45)
MapAccessMiss/Key=int32/Elem=bigType/len=6-4                      128.0n ± 0%   128.0n ± 0%        ~ (p=0.647 n=45)
MapAccessMiss/Key=int32/Elem=bigType/len=64-4                     129.7n ± 0%   130.2n ± 0%        ~ (p=0.069 n=45)
MapAccessMiss/Key=int32/Elem=bigType/len=65536-4                  154.3n ± 0%   154.3n ± 0%        ~ (p=0.058 n=45)
MapAccessMiss/Key=int32/Elem=*int32/len=6-4                       23.38n ± 1%   23.87n ± 1%   +2.10% (p=0.000 n=45)
MapAccessMiss/Key=int32/Elem=*int32/len=64-4                      25.52n ± 2%   25.41n ± 2%        ~ (p=0.321 n=45)
MapAccessMiss/Key=int32/Elem=*int32/len=65536-4                   38.28n ± 0%   37.95n ± 0%   -0.86% (p=0.000 n=45)
MapAccessZero/Key=int64-4                                         2.708n ± 0%   3.095n ± 0%  +14.29% (p=0.000 n=45)*
MapAccessZero/Key=int32-4                                         2.708n ± 0%   3.095n ± 0%  +14.29% (p=0.000 n=45)*
MapAccessZero/Key=string-4                                        3.481n ± 0%   3.095n ± 0%  -11.09% (p=0.000 n=45)*
MapAccessEmpty/Key=int64-4                                        3.095n ± 0%   3.096n ± 0%        ~ (p=0.087 n=45)*
MapAccessEmpty/Key=int32-4                                        3.095n ± 0%   3.482n ± 0%  +12.50% (p=0.000 n=45)*
MapAccessEmpty/Key=string-4                                       3.869n ± 0%   3.483n ± 0%   -9.98% (p=0.000 n=45)*
geomean                                                           28.04n        28.00n        -0.12%

* MapAccess(Empy|Zero) shows weird results due to basic block alignment changes

Fixes: #77892
Change-Id: I43913ae5dfa2d3e0cd173a766614ca4341774761
Reviewed-on: https://go-review.googlesource.com/c/go/+/750580
Auto-Submit: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
This commit is contained in:
ArsenySamoylov
2026-02-24 15:16:35 +03:00
committed by Gopher Robot
parent e7a09d1ffb
commit 252a8adbc0
4 changed files with 50 additions and 9 deletions

View File

@@ -14,6 +14,18 @@ import (
)
// Functions below pushed from runtime.
//
//go:noescape
//go:linkname memhash32 runtime.memhash32
func memhash32(p unsafe.Pointer, h uintptr) uintptr
//go:noescape
//go:linkname memhash64 runtime.memhash64
func memhash64(p unsafe.Pointer, h uintptr) uintptr
//go:noescape
//go:linkname strhash runtime.strhash
func strhash(a unsafe.Pointer, h uintptr) uintptr
//go:linkname fatal
func fatal(s string)

View File

@@ -52,8 +52,19 @@ func runtime_mapaccess2_fast32(typ *abi.MapType, m *Map, key uint32) (unsafe.Poi
return unsafe.Pointer(&zeroVal[0]), false
}
// Don't pass address of the key directly to the hashing function.
// Hashing functions are implemented in Go assembly and cannot be inlined,
// so compiler doesn't optimize redundant address taking/dereference.
//
// Taking &key makes compiler treat key as address-taken, which forces it to spill on the stack
// and reload it in the loop.
// This is suboptimal for performance.
//
// Note: Even when we pass k (local copy of key), the compiler still spills the key to the stack.
// However, from compiler's perspective, key is no longer address-taken and
// filled back in register before the loop.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash32(unsafe.Pointer(&k), m.seed)
// Select table.
idx := m.directoryIndex(hash)
@@ -141,8 +152,10 @@ func runtime_mapassign_fast32(typ *abi.MapType, m *Map, key uint32) unsafe.Point
fatal("concurrent map writes")
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash32(unsafe.Pointer(&k), m.seed)
// Set writing after calling Hasher, since Hasher may panic, in which
// case we have not actually done a write.
@@ -281,8 +294,10 @@ func runtime_mapassign_fast32ptr(typ *abi.MapType, m *Map, key unsafe.Pointer) u
fatal("concurrent map writes")
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash32(unsafe.Pointer(&k), m.seed)
// Set writing after calling Hasher, since Hasher may panic, in which
// case we have not actually done a write.

View File

@@ -52,8 +52,10 @@ func runtime_mapaccess2_fast64(typ *abi.MapType, m *Map, key uint64) (unsafe.Poi
return unsafe.Pointer(&zeroVal[0]), false
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash64(unsafe.Pointer(&k), m.seed)
// Select table.
idx := m.directoryIndex(hash)
@@ -142,8 +144,10 @@ func runtime_mapassign_fast64(typ *abi.MapType, m *Map, key uint64) unsafe.Point
fatal("concurrent map writes")
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash64(unsafe.Pointer(&k), m.seed)
// Set writing after calling Hasher, since Hasher may panic, in which
// case we have not actually done a write.
@@ -320,8 +324,10 @@ func runtime_mapassign_fast64ptr(typ *abi.MapType, m *Map, key unsafe.Pointer) u
fatal("concurrent map writes")
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := memhash64(unsafe.Pointer(&k), m.seed)
// Set writing after calling Hasher, since Hasher may panic, in which
// case we have not actually done a write.

View File

@@ -54,7 +54,11 @@ func (m *Map) getWithoutKeySmallFastStr(typ *abi.MapType, key string) unsafe.Poi
dohash:
// This path will cost 1 hash and 1+ε comparisons.
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&key)), m.seed)
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := strhash(unsafe.Pointer(&k), m.seed)
h2 := uint8(h2(hash))
ctrls = *g.ctrls()
slotKey = g.key(typ, 0)
@@ -128,8 +132,10 @@ func runtime_mapaccess2_faststr(typ *abi.MapType, m *Map, key string) (unsafe.Po
return elem, true
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := strhash(unsafe.Pointer(&k), m.seed)
// Select table.
idx := m.directoryIndex(hash)
@@ -219,8 +225,10 @@ func runtime_mapassign_faststr(typ *abi.MapType, m *Map, key string) unsafe.Poin
fatal("concurrent map writes")
}
// See the related comment in runtime_mapaccess2_fast32
// for why we pass local copy of key.
k := key
hash := typ.Hasher(abi.NoEscape(unsafe.Pointer(&k)), m.seed)
hash := strhash(unsafe.Pointer(&k), m.seed)
// Set writing after calling Hasher, since Hasher may panic, in which
// case we have not actually done a write.