Skip to content

Commit 5e6a868

Browse files
committed
cmd/compile, unique: model data flow of non-string pointers
Currently, hash/maphash.Comparable escapes its parameter if it contains non-string pointers, but does not escape strings or types that contain strings but no other pointers. This is achieved by a compiler intrinsic. unique.Make does something similar: it stores its parameter to a central map, with strings cloned. So from the escape analysis's perspective, the non-string pointers are passed through, whereas string pointers are not. We currently cannot model this type of type-dependent data flow directly in Go. So we do this with a compiler intrinsic. In fact, we can unify this and the intrinsic above. Tests are from Jake Bailey's CL 671955 (thanks!). Fixes #73680. Change-Id: Ia6a78e09dee39f8d9198a16758e4b5322ee2c56a Reviewed-on: https://go-review.googlesource.com/c/go/+/675156 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Jake Bailey <jacob.b.bailey@gmail.com>
1 parent 8bf816a commit 5e6a868

File tree

8 files changed

+119
-34
lines changed

8 files changed

+119
-34
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,19 @@ func (e *escape) call(ks []hole, call ir.Node) {
8484
argument(e.tagHole(ks, fn, param), arg)
8585
}
8686

87-
// hash/maphash.escapeForHash forces its argument to be on
88-
// the heap, if it contains a non-string pointer. We cannot
87+
// internal/abi.EscapeNonString forces its argument to be on
88+
// the heap, if it contains a non-string pointer.
89+
// This is used in hash/maphash.Comparable, where we cannot
8990
// hash pointers to local variables, as the address of the
9091
// local variable might change on stack growth.
9192
// Strings are okay as the hash depends on only the content,
9293
// not the pointer.
94+
// This is also used in unique.clone, to model the data flow
95+
// edge on the value with strings excluded, because strings
96+
// are cloned (by content).
9397
// The actual call we match is
94-
// hash/maphash.escapeForHash[go.shape.T](dict, go.shape.T)
95-
if fn != nil && fn.Sym().Pkg.Path == "hash/maphash" && strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
98+
// internal/abi.EscapeNonString[go.shape.T](dict, go.shape.T)
99+
if fn != nil && fn.Sym().Pkg.Path == "internal/abi" && strings.HasPrefix(fn.Sym().Name, "EscapeNonString[") {
96100
ps := fntype.Params()
97101
if len(ps) == 2 && ps[1].Type.IsShape() {
98102
if !hasNonStringPointers(ps[1].Type) {

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ opSwitch:
454454
// generate code.
455455
cheap = true
456456
}
457+
if strings.HasPrefix(fn, "EscapeNonString[") {
458+
// internal/abi.EscapeNonString[T] is a compiler intrinsic
459+
// implemented in the escape analysis phase.
460+
cheap = true
461+
}
457462
case "internal/runtime/sys":
458463
switch fn {
459464
case "GetCallerPC", "GetCallerSP":
@@ -472,12 +477,6 @@ opSwitch:
472477
case "panicrangestate":
473478
cheap = true
474479
}
475-
case "hash/maphash":
476-
if strings.HasPrefix(fn, "escapeForHash[") {
477-
// hash/maphash.escapeForHash[T] is a compiler intrinsic
478-
// implemented in the escape analysis phase.
479-
cheap = true
480-
}
481480
}
482481
}
483482
// Special case for coverage counter updates; although
@@ -801,10 +800,10 @@ func inlineCallCheck(callerfn *ir.Func, call *ir.CallExpr) (bool, bool) {
801800
}
802801
}
803802

804-
// hash/maphash.escapeForHash[T] is a compiler intrinsic implemented
803+
// internal/abi.EscapeNonString[T] is a compiler intrinsic implemented
805804
// in the escape analysis phase.
806-
if fn := ir.StaticCalleeName(call.Fun); fn != nil && fn.Sym().Pkg.Path == "hash/maphash" &&
807-
strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
805+
if fn := ir.StaticCalleeName(call.Fun); fn != nil && fn.Sym().Pkg.Path == "internal/abi" &&
806+
strings.HasPrefix(fn.Sym().Name, "EscapeNonString[") {
808807
return false, true
809808
}
810809

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,8 @@ func walkCall(n *ir.CallExpr, init *ir.Nodes) ir.Node {
594594

595595
if n.Op() == ir.OCALLFUNC {
596596
fn := ir.StaticCalleeName(n.Fun)
597-
if fn != nil && fn.Sym().Pkg.Path == "hash/maphash" && strings.HasPrefix(fn.Sym().Name, "escapeForHash[") {
598-
// hash/maphash.escapeForHash[T] is a compiler intrinsic
597+
if fn != nil && fn.Sym().Pkg.Path == "internal/abi" && strings.HasPrefix(fn.Sym().Name, "EscapeNonString[") {
598+
// internal/abi.EscapeNonString[T] is a compiler intrinsic
599599
// for the escape analysis to escape its argument based on
600600
// the type. The call itself is no-op. Just walk the
601601
// argument.

‎src/hash/maphash/maphash.go

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

1515
import (
1616
"hash"
17+
"internal/abi"
1718
"internal/byteorder"
1819
"math"
1920
)
@@ -293,26 +294,13 @@ func (h *Hash) Clone() (hash.Cloner, error) {
293294
// such that Comparable(s, v1) == Comparable(s, v2) if v1 == v2.
294295
// If v != v, then the resulting hash is randomly distributed.
295296
func Comparable[T comparable](seed Seed, v T) uint64 {
296-
escapeForHash(v)
297+
abi.EscapeNonString(v)
297298
return comparableHash(v, seed)
298299
}
299300

300-
// escapeForHash forces v to be on the heap, if v contains a
301-
// non-string pointer. We cannot hash pointers to local variables,
302-
// as the address of the local variable might change on stack growth.
303-
// Strings are okay as the hash depends on only the content, not
304-
// the pointer.
305-
//
306-
// This is essentially
307-
//
308-
// if hasNonStringPointers(T) { abi.Escape(v) }
309-
//
310-
// Implemented as a compiler intrinsic.
311-
func escapeForHash[T comparable](v T) { panic("intrinsic") }
312-
313301
// WriteComparable adds x to the data hashed by h.
314302
func WriteComparable[T comparable](h *Hash, x T) {
315-
escapeForHash(x)
303+
abi.EscapeNonString(x)
316304
// writeComparable (not in purego mode) directly operates on h.state
317305
// without using h.buf. Mix in the buffer length so it won't
318306
// commute with a buffered write, which either changes h.n or changes

‎src/internal/abi/escape.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,35 @@ func Escape[T any](x T) T {
3131
}
3232
return x
3333
}
34+
35+
// EscapeNonString forces v to be on the heap, if v contains a
36+
// non-string pointer.
37+
//
38+
// This is used in hash/maphash.Comparable. We cannot hash pointers
39+
// to local variables on stack, as their addresses might change on
40+
// stack growth. Strings are okay as the hash depends on only the
41+
// content, not the pointer.
42+
//
43+
// This is essentially
44+
//
45+
// if hasNonStringPointers(T) { Escape(v) }
46+
//
47+
// Implemented as a compiler intrinsic.
48+
func EscapeNonString[T any](v T) { panic("intrinsic") }
49+
50+
// EscapeToResultNonString models a data flow edge from v to the result,
51+
// if v contains a non-string pointer. If v contains only string pointers,
52+
// it returns a copy of v, but is not modeled as a data flow edge
53+
// from the escape analysis's perspective.
54+
//
55+
// This is used in unique.clone, to model the data flow edge on the
56+
// value with strings excluded, because strings are cloned (by
57+
// content).
58+
//
59+
// TODO: probably we should define this as a intrinsic and EscapeNonString
60+
// could just be "heap = EscapeToResultNonString(v)". This way we can model
61+
// an edge to the result but not necessarily heap.
62+
func EscapeToResultNonString[T any](v T) T {
63+
EscapeNonString(v)
64+
return *(*T)(NoEscape(unsafe.Pointer(&v)))
65+
}

‎src/unique/clone.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func clone[T comparable](value T, seq *cloneSeq) T {
2323
ps := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&value)) + offset))
2424
*ps = stringslite.Clone(*ps)
2525
}
26-
return value
26+
return abi.EscapeToResultNonString(value)
2727
}
2828

2929
// singleStringClone describes how to clone a single string.

‎src/unique/handle_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func TestMakeAllocs(t *testing.T) {
227227
stringHandle = Make(heapString)
228228
}},
229229

230-
{name: "stack string", allocs: 1, f: func() {
230+
{name: "stack string", allocs: 0, f: func() {
231231
var b [16]byte
232232
b[8] = 'a'
233233
stringHandle = Make(string(b[:]))
@@ -237,7 +237,7 @@ func TestMakeAllocs(t *testing.T) {
237237
stringHandle = Make(string(heapBytes))
238238
}},
239239

240-
{name: "bytes truncated short", allocs: 1, f: func() {
240+
{name: "bytes truncated short", allocs: 0, f: func() {
241241
stringHandle = Make(string(heapBytes[:16]))
242242
}},
243243

@@ -261,7 +261,7 @@ func TestMakeAllocs(t *testing.T) {
261261
pairHandle = Make([2]string{heapString, heapString})
262262
}},
263263

264-
{name: "pair from stack", allocs: 2, f: func() {
264+
{name: "pair from stack", allocs: 0, f: func() {
265265
var b [16]byte
266266
b[8] = 'a'
267267
pairHandle = Make([2]string{string(b[:]), string(b[:])})

‎test/escape_unique.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// errorcheck -0 -m -l
2+
3+
// Copyright 2025 The Go Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE file.
6+
7+
// Test escape analysis for unique.
8+
9+
package escape
10+
11+
import "unique"
12+
13+
type T string
14+
15+
func f1(s string) unique.Handle[string] { // ERROR "s does not escape$"
16+
return unique.Make(s)
17+
}
18+
19+
func f1a(s []byte) unique.Handle[string] { // ERROR "s does not escape$"
20+
return unique.Make(string(s)) // ERROR "string\(s\) does not escape$"
21+
}
22+
23+
func gen[S ~string](s S) unique.Handle[S] {
24+
return unique.Make(s)
25+
}
26+
27+
func f2(s T) unique.Handle[T] { // ERROR "s does not escape$"
28+
return unique.Make(s)
29+
}
30+
31+
func f3(s T) unique.Handle[T] { // ERROR "s does not escape$"
32+
return gen(s)
33+
}
34+
35+
type pair struct {
36+
s1 string
37+
s2 string
38+
}
39+
40+
func f4(s1 string, s2 string) unique.Handle[pair] { // ERROR "s1 does not escape$" "s2 does not escape$"
41+
return unique.Make(pair{s1, s2})
42+
}
43+
44+
type viaInterface struct {
45+
s any
46+
}
47+
48+
func f5(s string) unique.Handle[viaInterface] { // ERROR "leaking param: s$"
49+
return unique.Make(viaInterface{s}) // ERROR "s escapes to heap$"
50+
}
51+
52+
var sink any
53+
54+
func f6(s string) unique.Handle[string] { // ERROR "leaking param: s$"
55+
sink = s // ERROR "s escapes to heap$"
56+
return unique.Make(s)
57+
}
58+
59+
func f6a(s []byte) unique.Handle[string] { // ERROR "leaking param: s$"
60+
sink = s // ERROR "s escapes to heap$"
61+
return unique.Make(string(s)) // ERROR "string\(s\) does not escape$"
62+
}

0 commit comments

Comments
 (0)