compiler, runtime, reflect: generate type-specific hash/equal (#5359)

* compiler, runtime, reflect: generate type-specific hash/equal for composite map keys

For map keys that are not trivially binary-comparable, the compiler now
generates type-specific hash and equal functions as LLVM IR instead of
going through the interface+reflection path. This covers comparable
types: strings, floats, complex numbers, interfaces, channels, and
composites containing any mix of these.

Previously, maps with composite keys containing strings or floats
converted the key to interface{}, hashed via reflection, and compared
through interface equality. Now the compiler walks struct fields and
array elements directly, dispatching to the right runtime helper for
each field type and storing keys at their actual type.

Struct keys are always handled field-by-field so padding bytes do not
affect equality or hashing. Blank fields are ignored, matching Go
equality. Generated hash/equal function names use canonical underlying
type structure so structurally identical key types can share generated
functions. Padding zeroing before map operations is no longer needed
because structs no longer use the binary key path.

Also fix reflect map iteration for interface-keyed maps: MapIter.Key
returns an interface Value for map[interface{}] keys instead of
unpacking to the concrete key kind.

* compiler: generate loops for array map key hash/equal

Previously, array key hash and equal functions were unrolled at compile
time, generating one block of IR per element. For large arrays like
[1000]int inside a struct with non-binary fields, this caused code
explosion.

Now, binary-element arrays dispatch directly to hash32/memequal for the
whole array. Non-binary-element arrays generate an LLVM IR loop. The
equal loop short-circuits on the first mismatch.

Small arrays are still unrolled instead of looping, keeping the simple
cases compact.

* reflect: fix at-runtime map issues from review, and more found locally

Maps created through reflect.MakeMap need hash/equal behavior that
matches compiler-created maps. Add hashmapMakeReflect for composite key
types, using runtime closures that reconstruct interface{} values from
raw key bytes and delegate to the interface hash and equality paths.

Interface-keyed maps are already stored as interface values, so use the
existing interface hash/equal helpers directly for those. This keeps
reflect insert, lookup, delete, and compiled lookup paths consistent.

Also fix addressable small values used as interface map keys or
interface map values. loadSmallValue puts small indirect values back in
the pointer-sized interface data field the same way valueInterfaceUnsafe
does.

* compiler, interp, reflect: fix pointer map literals; remove interface fallback

Package-level map literals with pointer keys (both *T and
unsafe.Pointer) crash the compiler: the interp pass panics when trying
to hash pointer data as raw bytes, because pointer values in the interp
memory model are symbolic identities that do not fit in a byte.

Fix this by setting a recoverable error flag instead of panicking. The
interp detects the error after each instruction and defers the map
insert to runtime init code, where real addresses are available for
hashing. This matches how the interp already handles other operations
it cannot evaluate at compile time.

With this fix, unsafe.Pointer can also be classified as a binary map
key, which was the last type requiring the interface-based fallback.
Since all comparable types now use either the binary or the
compiler-generated hash/equal path, remove the interface fallback from
the compiler and reflect packages.

* compiler, transform: always pass hash/equal function pointers to hashmapMakeGeneric

The compiler now always resolves the hash and equal functions at compile
time and passes them directly to hashmapMakeGeneric, instead of passing
an algorithm enum to hashmapMake and resolving at runtime. For string
keys, the runtime hashmapStringPtrHash/hashmapStringEqual functions are
referenced directly. For binary keys, hash32/memequal are referenced.

The old hashmapMake with alg enum is retained for reflect, which still
needs runtime resolution when creating maps dynamically.

The OptimizeMaps transform pass is updated to handle both hashmapMake
and hashmapMakeGeneric, and to recognize hashmapGenericSet in addition
to hashmapBinarySet and hashmapStringSet. The now-unused
hashmapCanGenerateHashEqual helper is removed.

* runtime: store large map keys and values indirectly

When a map key or value exceeds 128 bytes, the bucket now stores a
pointer to separately allocated memory instead of the data inline. This
matches Go's MapMaxKeyBytes/MapMaxElemBytes threshold and prevents
bucket sizes from exploding for large key/value types.

For example, map[[256]byte]int previously used 2128 bytes per bucket
(16 header + 256*8 keys + 8*8 values); now it uses 144 bytes per bucket
(16 header + 8*8 pointers + 8*8 values).

The indirection is fully encapsulated in the runtime via helper
functions. Store the computed key and value slot sizes on the hashmap so
all runtime and reflect paths use the same bucket layout, including
non-indirect keys and values.

Add big-key golden coverage and benchmarks. Make the benchmark vary
enough key bytes to exercise hashing.
This commit is contained in:
Jake Bailey
2026-05-18 04:31:27 -07:00
committed by GitHub
parent 89d9e33bca
commit 18033ebc36
19 changed files with 1303 additions and 358 deletions
+85
View File
@@ -29,6 +29,14 @@ var testMapArrayKey = map[ArrayKey]int{
}
var testmapIntInt = map[int]int{1: 1, 2: 4, 3: 9}
// Package-level pointer map literals: these exercise the interp pass's
// ability to defer pointer-keyed map inserts to runtime (since pointer
// hashes can't be computed at compile time).
var testPtrMapVar1 = 42
var testPtrMapVar2 = 99
var testPtrMap = map[*int]int{&testPtrMapVar1: 1, &testPtrMapVar2: 2}
var testUnsafePtrMap = map[unsafe.Pointer]int{unsafe.Pointer(&testPtrMapVar1): 10}
type namedFloat struct {
s string
f float32
@@ -130,6 +138,10 @@ func main() {
mapgrow()
nestedarraymaps()
ptrmaps()
interfacerehash()
}
@@ -308,3 +320,76 @@ func interfacerehash() {
println("no interface lookup failures")
}
}
func nestedarraymaps() {
type nestedArrayElem struct {
x uint8
}
type nestedArrayKey struct {
a [5][5]nestedArrayElem
}
var k nestedArrayKey
k.a[4][4].x = 7
m := map[nestedArrayKey]int{k: 42}
println("nested array key:", m[k])
}
func ptrmaps() {
// Package-level pointer map literals (interp defers inserts to runtime).
println("ptr map literal:", testPtrMap[&testPtrMapVar1], testPtrMap[&testPtrMapVar2])
println("unsafe ptr literal:", testUnsafePtrMap[unsafe.Pointer(&testPtrMapVar1)])
// Runtime pointer maps.
a, b, c := 1, 2, 3
m := make(map[*int]int)
m[&a] = 10
m[&b] = 20
m[&c] = 30
println("ptr map len:", len(m))
println("ptr map a:", m[&a])
delete(m, &b)
_, ok := m[&b]
println("ptr map deleted:", ok)
// Runtime unsafe.Pointer maps.
m2 := make(map[unsafe.Pointer]int)
m2[unsafe.Pointer(&a)] = 100
println("unsafe ptr map:", m2[unsafe.Pointer(&a)])
// Struct keys with padding: the hash/equal must operate per-field
// and not include padding bytes. Only test when padding actually
// exists (e.g., not on AVR where alignment is 1).
type paddedKey struct {
A int8
B int32
}
if unsafe.Offsetof(paddedKey{}.B) > 1 {
pm := make(map[paddedKey]int)
var pk1, pk2 paddedKey
pk1.A = 1; pk1.B = 42
pk2.A = 1; pk2.B = 42
// Poison pk2's padding byte (between A and B).
*(*byte)(unsafe.Add(unsafe.Pointer(&pk2), 1)) = 0xFF
pm[pk1] = 100
println("padded key lookup:", pm[pk2]) // 100
println("padded key equal:", pk1 == pk2) // true
} else {
// No padding on this platform; print expected output.
println("padded key lookup:", 100)
println("padded key equal:", true)
}
// Struct keys with blank fields: blank fields are ignored in equality.
type blankKey struct {
_ int
X string
}
bm := make(map[blankKey]int)
var bk1, bk2 blankKey
bk1.X = "hello"
bk2.X = "hello"
*(*int)(unsafe.Pointer(&bk2)) = 999
bm[bk1] = 200
println("blank key lookup:", bm[bk2]) // 200
println("blank key equal:", bk1 == bk2) // true
}
+11
View File
@@ -80,4 +80,15 @@ tested growing of a map
2
2
done
nested array key: 42
ptr map literal: 1 2
unsafe ptr literal: 10
ptr map len: 3
ptr map a: 10
ptr map deleted: false
unsafe ptr map: 100
padded key lookup: 100
padded key equal: true
blank key lookup: 200
blank key equal: true
no interface lookup failures
+63
View File
@@ -0,0 +1,63 @@
package main
// Test maps with keys and values larger than 128 bytes, which triggers
// indirect storage in the bucket (pointers instead of inline data).
//
// This is a separate file from map.go because the compiler generates many
// large stack temporaries for map operations on [256]byte keys, which
// overflows the goroutine stack on AVR (384 bytes). AVR skips this test.
type BigKey [256]byte
type BigValue [256]byte
func main() {
// Large key, small value.
m1 := make(map[BigKey]int)
var k1 BigKey
k1[0] = 1
k1[255] = 42
m1[k1] = 100
var k1same BigKey
k1same[0] = 1
k1same[255] = 42
var k1diff BigKey
k1diff[0] = 2
println("bigkey get:", m1[k1])
println("bigkey get same:", m1[k1same])
println("bigkey get diff:", m1[k1diff])
// Overwrite.
m1[k1] = 200
println("bigkey overwrite:", m1[k1])
// Small key, large value.
m2 := make(map[int]BigValue)
var v BigValue
v[0] = 7
v[255] = 99
m2[1] = v
got := m2[1]
println("bigval get:", got[0], got[255])
// Both large.
m3 := make(map[BigKey]BigValue)
m3[k1] = v
got3 := m3[k1]
println("bigboth get:", got3[0], got3[255])
// Delete.
delete(m3, k1)
got3 = m3[k1]
println("bigboth deleted:", got3[0])
// Iteration.
m1[k1diff] = 300
count := 0
for range m1 {
count++
}
println("bigkey len:", len(m1), "iterated:", count)
}
+8
View File
@@ -0,0 +1,8 @@
bigkey get: 100
bigkey get same: 100
bigkey get diff: 0
bigkey overwrite: 200
bigval get: 7 99
bigboth get: 7 99
bigboth deleted: 0
bigkey len: 2 iterated: 2
+140
View File
@@ -396,6 +396,24 @@ func main() {
}
}
}
// Test for issue #3794: reflect MapIter.Key() should return a value with
// interface kind for map[interface{}] keys, not the underlying concrete kind.
{
m := make(map[interface{}]int)
m[1] = 2
m["hello"] = 3
rv := reflect.ValueOf(m)
iter := rv.MapRange()
for iter.Next() {
k := iter.Key()
if k.Kind() != reflect.Interface {
println("FAIL #3794: expected interface kind, got", k.Kind().String())
break
}
}
println("reflect map interface key ok")
}
}
func emptyFunc() {
@@ -755,6 +773,9 @@ func testImplements() {
// Make FooNode and BarNode implement Node with pointer receivers
// (can't add methods to local types in function, use a different approach)
testValueSetInterface()
testMakeMapCompositeKey()
testMakeMapInterfaceKey()
testMakeMapPaddedKey()
}
type IfaceNode interface {
@@ -807,3 +828,122 @@ func randuint32() uint32 {
xorshift32State = xorshift32(xorshift32State)
return xorshift32State
}
type compositeKey struct {
S string
N int32
}
// testMakeMapCompositeKey tests that reflect.MakeMap works correctly with
// composite key types (structs containing strings). This exercises the
// hash/equal dispatch path for maps created through reflection rather
// than by the compiler.
func testMakeMapCompositeKey() {
println("\nreflect.MakeMap composite key:")
mapType := reflect.TypeOf(map[compositeKey]int{})
m := reflect.MakeMap(mapType)
// Insert two keys that share the same string but differ in the int field.
key1 := reflect.ValueOf(compositeKey{S: "hello", N: 1})
key2 := reflect.ValueOf(compositeKey{S: "hello", N: 2})
m.SetMapIndex(key1, reflect.ValueOf(100))
m.SetMapIndex(key2, reflect.ValueOf(200))
println("len:", m.Len())
v1 := m.MapIndex(key1)
if v1.IsValid() {
println("key1:", v1.Int())
} else {
println("key1: not found")
}
v2 := m.MapIndex(key2)
if v2.IsValid() {
println("key2:", v2.Int())
} else {
println("key2: not found")
}
// Delete key1, verify key2 remains.
m.SetMapIndex(key1, reflect.Value{})
println("after delete, len:", m.Len())
v2 = m.MapIndex(key2)
if v2.IsValid() {
println("key2 after delete:", v2.Int())
} else {
println("key2 after delete: not found")
}
}
// testMakeMapInterfaceKey tests that reflect.MakeMap works correctly with
// interface{} key types, including cross-path usage (reflect insert,
// compiled lookup and vice versa).
func testMakeMapInterfaceKey() {
println("\nreflect.MakeMap interface key:")
mapType := reflect.TypeOf(map[interface{}]int{})
rv := reflect.MakeMap(mapType)
rv.SetMapIndex(reflect.ValueOf(42), reflect.ValueOf(100))
rv.SetMapIndex(reflect.ValueOf("hello"), reflect.ValueOf(200))
println("len:", rv.Len())
v1 := rv.MapIndex(reflect.ValueOf(42))
if v1.IsValid() {
println("42:", v1.Int())
} else {
println("42: not found")
}
v2 := rv.MapIndex(reflect.ValueOf("hello"))
if v2.IsValid() {
println("hello:", v2.Int())
} else {
println("hello: not found")
}
// Cross-path: use from compiled code.
m := rv.Interface().(map[interface{}]int)
println("compiled 42:", m[42])
println("compiled hello:", m["hello"])
// Addressable small value as key.
x := 99
addrVal := reflect.ValueOf(&x).Elem()
rv.SetMapIndex(addrVal, reflect.ValueOf(300))
v3 := rv.MapIndex(reflect.ValueOf(99))
if v3.IsValid() {
println("addressable 99:", v3.Int())
} else {
println("addressable 99: not found")
}
}
type paddedKey struct {
A int8
B int32
}
// testMakeMapPaddedKey tests that struct keys with padding work correctly
// through reflect, using addressable values with poisoned padding bytes.
func testMakeMapPaddedKey() {
println("\nreflect.MakeMap padded key:")
var pk1, pk2 paddedKey
pk1.A = 1
pk1.B = 42
pk2.A = 1
pk2.B = 42
if unsafe.Offsetof(paddedKey{}.B) > 1 {
// Poison pk2's padding byte (between A and B).
*(*byte)(unsafe.Add(unsafe.Pointer(&pk2), 1)) = 0xFF
}
// Use addressable values so padding survives into reflect.
rm := reflect.MakeMap(reflect.TypeOf(map[paddedKey]int{}))
rm.SetMapIndex(reflect.ValueOf(&pk1).Elem(), reflect.ValueOf(100))
v := rm.MapIndex(reflect.ValueOf(&pk2).Elem())
if v.IsValid() {
println("padded lookup:", v.Int())
} else {
println("padded lookup: not found")
}
}
+19
View File
@@ -503,6 +503,24 @@ value set interface:
Set[0] to BarNode: 10
Set[1] still FooNode: 2
reflect.MakeMap composite key:
len: 2
key1: 100
key2: 200
after delete, len: 1
key2 after delete: 200
reflect.MakeMap interface key:
len: 2
42: 100
hello: 200
compiled 42: 100
compiled hello: 200
addressable 99: 300
reflect.MakeMap padded key:
padded lookup: 100
alignment / offset:
struct{[0]func(); byte}: true
@@ -512,3 +530,4 @@ blue gopher
v.Interface() method
kind: interface
int 5
reflect map interface key ok