Skip to content

Commit 50a8b3a

Browse files
committed
hash/maphash, cmd/compile: make Comparable[string] not escape its argument
Currently, maphash.Comparable forces its argument to escape if it contains a pointer, as we cannot hash stack pointers, which will change when the stack moves. However, for a string, it is actually okay if its data pointer points to the stack, as the hash depends on only the content, not the pointer. Currently there is no way to write this type-dependent escape logic in Go code. So we implement it in the compiler as an intrinsic. The compiler can also recognize not just the string type, but types whose pointers are all string pointers, and make them not escape. Fixes #70560. Change-Id: I3bf219ad71a238d2e35f0ea33de96487bc8cc231 Reviewed-on: https://go-review.googlesource.com/c/go/+/632715 Reviewed-by: David Chase <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7632c6e commit 50a8b3a

File tree

7 files changed

+122
-10
lines changed

7 files changed

+122
-10
lines changed

src/cmd/compile/internal/escape/call.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"cmd/compile/internal/typecheck"
1111
"cmd/compile/internal/types"
1212
"cmd/internal/src"
13+
"strings"
1314
)
1415

1516
// call evaluates a call expressions, including builtin calls. ks
@@ -82,6 +83,29 @@ func (e *escape) call(ks []hole, call ir.Node) {
8283
argument(e.tagHole(ks, fn, param), arg)
8384
}
8485

86+
// hash/maphash.escapeForHash forces its argument to be on
87+
// the heap, if it contains a non-string pointer. We cannot
88+
// hash pointers to local variables, as the address of the
89+
// local variable might change on stack growth.
90+
// Strings are okay as the hash depends on only the content,
91+
// not the pointer.
92+
// The actual call we match is
93+
// hash/maphash.escapeForHash[go.shape.T](dict, go.shape.T)
94+
if fn != nil && fn.Sym().Pkg.Path == "hash/maphash" && strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
95+
ps := fntype.Params()
96+
if len(ps) == 2 && ps[1].Type.IsShape() {
97+
if !hasNonStringPointers(ps[1].Type) {
98+
argumentParam = func(param *types.Field, arg ir.Node) {
99+
argument(e.discardHole(), arg)
100+
}
101+
} else {
102+
argumentParam = func(param *types.Field, arg ir.Node) {
103+
argument(e.heapHole(), arg)
104+
}
105+
}
106+
}
107+
}
108+
85109
args := call.Args
86110
if recvParam := fntype.Recv(); recvParam != nil {
87111
if recvArg == nil {
@@ -359,3 +383,23 @@ func (e *escape) tagHole(ks []hole, fn *ir.Name, param *types.Field) hole {
359383

360384
return e.teeHole(tagKs...)
361385
}
386+
387+
func hasNonStringPointers(t *types.Type) bool {
388+
if !t.HasPointers() {
389+
return false
390+
}
391+
switch t.Kind() {
392+
case types.TSTRING:
393+
return false
394+
case types.TSTRUCT:
395+
for _, f := range t.Fields() {
396+
if hasNonStringPointers(f.Type) {
397+
return true
398+
}
399+
}
400+
return false
401+
case types.TARRAY:
402+
return hasNonStringPointers(t.Elem())
403+
}
404+
return true
405+
}

src/cmd/compile/internal/inline/inl.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"go/constant"
3232
"internal/buildcfg"
3333
"strconv"
34+
"strings"
3435

3536
"cmd/compile/internal/base"
3637
"cmd/compile/internal/inline/inlheur"
@@ -481,6 +482,12 @@ opSwitch:
481482
case "panicrangestate":
482483
cheap = true
483484
}
485+
case "hash/maphash":
486+
if strings.HasPrefix(fn, "escapeForHash[") {
487+
// hash/maphash.escapeForHash[T] is a compiler intrinsic
488+
// implemented in the escape analysis phase.
489+
cheap = true
490+
}
484491
}
485492
}
486493
// Special case for coverage counter updates; although
@@ -803,6 +810,14 @@ func inlineCallCheck(callerfn *ir.Func, call *ir.CallExpr) (bool, bool) {
803810
}
804811
}
805812
}
813+
814+
// hash/maphash.escapeForHash[T] is a compiler intrinsic implemented
815+
// in the escape analysis phase.
816+
if fn := ir.StaticCalleeName(call.Fun); fn != nil && fn.Sym().Pkg.Path == "hash/maphash" &&
817+
strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
818+
return false, true
819+
}
820+
806821
if ir.IsIntrinsicCall(call) {
807822
return false, true
808823
}

src/cmd/compile/internal/walk/expr.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,20 @@ func walkCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
582582
return walkExpr(n, init)
583583
}
584584

585+
if n.Op() == ir.OCALLFUNC {
586+
fn := ir.StaticCalleeName(n.Fun)
587+
if fn != nil && fn.Sym().Pkg.Path == "hash/maphash" && strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
588+
// hash/maphash.escapeForHash[T] is a compiler intrinsic
589+
// for the escape analysis to escape its argument based on
590+
// the type. The call itself is no-op. Just walk the
591+
// argument.
592+
ps := fn.Type().Params()
593+
if len(ps) == 2 && ps[1].Type.IsShape() {
594+
return walkExpr(n.Args[1], init)
595+
}
596+
}
597+
}
598+
585599
if name, ok := n.Fun.(*ir.Name); ok {
586600
sym := name.Sym()
587601
if sym.Pkg.Path == "go.runtime" && sym.Name == "deferrangefunc" {

src/hash/maphash/maphash.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
package maphash
1414

1515
import (
16-
"internal/abi"
1716
"internal/byteorder"
1817
"math"
1918
)
@@ -286,21 +285,26 @@ func (h *Hash) BlockSize() int { return len(h.buf) }
286285
// such that Comparable(s, v1) == Comparable(s, v2) if v1 == v2.
287286
// If v != v, then the resulting hash is randomly distributed.
288287
func Comparable[T comparable](seed Seed, v T) uint64 {
289-
comparableReady(v)
288+
escapeForHash(v)
290289
return comparableHash(v, seed)
291290
}
292291

293-
func comparableReady[T comparable](v T) {
294-
// Force v to be on the heap.
295-
// We cannot hash pointers to local variables,
296-
// as the address of the local variable
297-
// might change on stack growth.
298-
abi.Escape(v)
299-
}
292+
// escapeForHash forces v to be on the heap, if v contains a
293+
// non-string pointer. We cannot hash pointers to local variables,
294+
// as the address of the local variable might change on stack growth.
295+
// Strings are okay as the hash depends on only the content, not
296+
// the pointer.
297+
//
298+
// This is essentially
299+
//
300+
// if hasNonStringPointers(T) { abi.Escape(v) }
301+
//
302+
// Implemented as a compiler intrinsic.
303+
func escapeForHash[T comparable](v T) { panic("intrinsic") }
300304

301305
// WriteComparable adds x to the data hashed by h.
302306
func WriteComparable[T comparable](h *Hash, x T) {
303-
comparableReady(x)
307+
escapeForHash(x)
304308
// writeComparable (not in purego mode) directly operates on h.state
305309
// without using h.buf. Mix in the buffer length so it won't
306310
// commute with a buffered write, which either changes h.n or changes

src/hash/maphash/maphash_purego.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"reflect"
1515
)
1616

17+
const purego = true
18+
1719
var hashkey [4]uint64
1820

1921
func init() {

src/hash/maphash/maphash_runtime.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"unsafe"
1414
)
1515

16+
const purego = false
17+
1618
//go:linkname runtime_rand runtime.rand
1719
func runtime_rand() uint64
1820

src/hash/maphash/maphash_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"fmt"
1010
"hash"
11+
"internal/asan"
1112
"math"
1213
"reflect"
1314
"strings"
@@ -420,6 +421,36 @@ func TestWriteComparableNoncommute(t *testing.T) {
420421
}
421422
}
422423

424+
func TestComparableAllocations(t *testing.T) {
425+
if purego {
426+
t.Skip("skip allocation test in purego mode - reflect-based implementation allocates more")
427+
}
428+
if asan.Enabled {
429+
t.Skip("skip allocation test under -asan")
430+
}
431+
seed := MakeSeed()
432+
x := heapStr(t)
433+
allocs := testing.AllocsPerRun(10, func() {
434+
s := "s" + x
435+
Comparable(seed, s)
436+
})
437+
if allocs > 0 {
438+
t.Errorf("got %v allocs, want 0", allocs)
439+
}
440+
441+
type S struct {
442+
a int
443+
b string
444+
}
445+
allocs = testing.AllocsPerRun(10, func() {
446+
s := S{123, "s" + x}
447+
Comparable(seed, s)
448+
})
449+
if allocs > 0 {
450+
t.Errorf("got %v allocs, want 0", allocs)
451+
}
452+
}
453+
423454
// Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces.
424455
var _ hash.Hash = &Hash{}
425456
var _ hash.Hash64 = &Hash{}

0 commit comments

Comments
 (0)