Skip to content

Commit ff47dd1

Browse files
mdempskygopherbot
authored andcommitted
cmd/compile/internal/escape: optimize indirect closure calls
This CL extends escape analysis in two ways. First, we already optimize directly called closures. For example, given: var x int // already stack allocated today p := func() *int { return &x }() we don't need to move x to the heap, because we can statically track where &x flows. This CL extends the same idea to work for indirectly called closures too, as long as we know everywhere that they're called. For example: var x int // stack allocated after this CL f := func() *int { return &x } p := f() This will allow a subsequent CL to move the generation of go/defer wrappers earlier. Second, this CL adds tracking to detect when pointer values flow to the pointee operand of an indirect assignment statement (i.e., flows to p in "*p = x") or to builtins that modify memory (append, copy, clear). This isn't utilized in the current CL, but a subsequent CL will make use of it to better optimize string->[]byte conversions. Updates #2205. Change-Id: I610f9c531e135129c947684833e288ce64406f35 Reviewed-on: https://go-review.googlesource.com/c/go/+/520259 Run-TryBot: Matthew Dempsky <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Auto-Submit: Matthew Dempsky <[email protected]> Reviewed-by: Cuong Manh Le <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent f278ae6 commit ff47dd1

File tree

9 files changed

+268
-97
lines changed

9 files changed

+268
-97
lines changed

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ func (e *escape) addr(n ir.Node) hole {
3939
if n.X.Type().IsArray() {
4040
k = e.addr(n.X)
4141
} else {
42-
e.discard(n.X)
42+
e.mutate(n.X)
4343
}
4444
case ir.ODEREF, ir.ODOTPTR:
45-
e.discard(n)
45+
e.mutate(n)
4646
case ir.OINDEXMAP:
4747
n := n.(*ir.IndexExpr)
4848
e.discard(n.X)
@@ -52,6 +52,10 @@ func (e *escape) addr(n ir.Node) hole {
5252
return k
5353
}
5454

55+
func (e *escape) mutate(n ir.Node) {
56+
e.expr(e.mutatorHole(), n)
57+
}
58+
5559
func (e *escape) addrs(l ir.Nodes) []hole {
5660
var ks []hole
5761
for _, n := range l {

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

+33-16
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,8 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
6868
var fn *ir.Name
6969
switch call.Op() {
7070
case ir.OCALLFUNC:
71-
// If we have a direct call to a closure (not just one we were
72-
// able to statically resolve with ir.StaticValue), mark it as
73-
// such so batch.outlives can optimize the flow results.
74-
if call.X.Op() == ir.OCLOSURE {
75-
call.X.(*ir.ClosureExpr).Func.SetClosureCalled(true)
76-
}
77-
7871
v := ir.StaticValue(call.X)
7972
fn = ir.StaticCalleeName(v)
80-
case ir.OCALLMETH:
81-
base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck")
8273
}
8374

8475
fntype := call.X.Type()
@@ -88,7 +79,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
8879

8980
if ks != nil && fn != nil && e.inMutualBatch(fn) {
9081
for i, result := range fn.Type().Results().FieldSlice() {
91-
e.expr(ks[i], ir.AsNode(result.Nname))
82+
e.expr(ks[i], result.Nname.(*ir.Name))
9283
}
9384
}
9485

@@ -99,7 +90,20 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
9990
// Note: We use argument and not argumentFunc, because while
10091
// call.X here may be an argument to runtime.{new,defer}proc,
10192
// it's not an argument to fn itself.
102-
argument(e.discardHole(), &call.X)
93+
calleeK := e.discardHole()
94+
if fn == nil { // unknown callee
95+
for _, k := range ks {
96+
if k.dst != &e.blankLoc {
97+
// The results flow somewhere, but we don't statically
98+
// know the callee function. If a closure flows here, we
99+
// need to conservatively assume its results might flow to
100+
// the heap.
101+
calleeK = e.calleeHole()
102+
break
103+
}
104+
}
105+
}
106+
argument(calleeK, &call.X)
103107
} else {
104108
recvp = &call.X.(*ir.SelectorExpr).X
105109
}
@@ -139,7 +143,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
139143
// it has enough capacity. Alternatively, a new heap
140144
// slice might be allocated, and all slice elements
141145
// might flow to heap.
142-
appendeeK := ks[0]
146+
appendeeK := e.teeHole(ks[0], e.mutatorHole())
143147
if args[0].Type().Elem().HasPointers() {
144148
appendeeK = e.teeHole(appendeeK, e.heapHole().deref(call, "appendee slice"))
145149
}
@@ -160,7 +164,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
160164

161165
case ir.OCOPY:
162166
call := call.(*ir.BinaryExpr)
163-
argument(e.discardHole(), &call.X)
167+
argument(e.mutatorHole(), &call.X)
164168

165169
copiedK := e.discardHole()
166170
if call.Y.Type().IsSlice() && call.Y.Type().Elem().HasPointers() {
@@ -185,10 +189,14 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
185189
}
186190
argumentRType(&call.RType)
187191

188-
case ir.OLEN, ir.OCAP, ir.OREAL, ir.OIMAG, ir.OCLOSE, ir.OCLEAR:
192+
case ir.OLEN, ir.OCAP, ir.OREAL, ir.OIMAG, ir.OCLOSE:
189193
call := call.(*ir.UnaryExpr)
190194
argument(e.discardHole(), &call.X)
191195

196+
case ir.OCLEAR:
197+
call := call.(*ir.UnaryExpr)
198+
argument(e.mutatorHole(), &call.X)
199+
192200
case ir.OUNSAFESTRINGDATA, ir.OUNSAFESLICEDATA:
193201
call := call.(*ir.UnaryExpr)
194202
argument(ks[0], &call.X)
@@ -251,6 +259,7 @@ func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
251259
fn := ir.NewClosureFunc(n.Pos(), true)
252260
fn.SetWrapper(true)
253261
fn.Nname.SetType(types.NewSignature(nil, nil, nil))
262+
fn.SetEsc(escFuncTagged) // no params; effectively tagged already
254263
fn.Body = []ir.Node{call}
255264
if call, ok := call.(*ir.CallExpr); ok && call.Op() == ir.OCALLFUNC {
256265
// If the callee is a named function, link to the original callee.
@@ -310,9 +319,11 @@ func (e *escape) rewriteArgument(argp *ir.Node, init *ir.Nodes, call ir.Node, fn
310319
// Create and declare a new pointer-typed temp variable.
311320
tmp := e.wrapExpr(arg.Pos(), &arg.X, init, call, wrapper)
312321

322+
k := e.mutatorHole()
313323
if pragma&ir.UintptrEscapes != 0 {
314-
e.flow(e.heapHole().note(arg, "//go:uintptrescapes"), e.oldLoc(tmp))
324+
k = e.heapHole().note(arg, "//go:uintptrescapes")
315325
}
326+
e.flow(k, e.oldLoc(tmp))
316327

317328
if pragma&ir.UintptrKeepAlive != 0 {
318329
call := call.(*ir.CallExpr)
@@ -454,11 +465,17 @@ func (e *escape) tagHole(ks []hole, fn *ir.Name, param *types.Field) hole {
454465
// Call to previously tagged function.
455466

456467
var tagKs []hole
457-
458468
esc := parseLeaks(param.Note)
469+
459470
if x := esc.Heap(); x >= 0 {
460471
tagKs = append(tagKs, e.heapHole().shift(x))
461472
}
473+
if x := esc.Mutator(); x >= 0 {
474+
tagKs = append(tagKs, e.mutatorHole().shift(x))
475+
}
476+
if x := esc.Callee(); x >= 0 {
477+
tagKs = append(tagKs, e.calleeHole().shift(x))
478+
}
462479

463480
if ks != nil {
464481
for i := 0; i < numEscResults; i++ {

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

+45-8
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ type batch struct {
8888
allLocs []*location
8989
closures []closure
9090

91-
heapLoc location
92-
blankLoc location
91+
heapLoc location
92+
mutatorLoc location
93+
calleeLoc location
94+
blankLoc location
9395
}
9496

9597
// A closure holds a closure expression and its spill hole (i.e.,
@@ -129,7 +131,9 @@ func Batch(fns []*ir.Func, recursive bool) {
129131
}
130132

131133
var b batch
132-
b.heapLoc.attrs = attrEscapes | attrPersists
134+
b.heapLoc.attrs = attrEscapes | attrPersists | attrMutates | attrCalls
135+
b.mutatorLoc.attrs = attrMutates
136+
b.calleeLoc.attrs = attrCalls
133137

134138
// Construct data-flow graph from syntax trees.
135139
for _, fn := range fns {
@@ -288,6 +292,7 @@ func (b *batch) finish(fns []*ir.Func) {
288292
if n == nil {
289293
continue
290294
}
295+
291296
if n.Op() == ir.ONAME {
292297
n := n.(*ir.Name)
293298
n.Opt = nil
@@ -337,6 +342,20 @@ func (b *batch) finish(fns []*ir.Func) {
337342
}
338343
}
339344
}
345+
346+
// If the result of a string->[]byte conversion is never mutated,
347+
// then it can simply reuse the string's memory directly.
348+
//
349+
// TODO(mdempsky): Enable in a subsequent CL. We need to ensure
350+
// []byte("") evaluates to []byte{}, not []byte(nil).
351+
if false {
352+
if n, ok := n.(*ir.ConvExpr); ok && n.Op() == ir.OSTR2BYTES && !loc.hasAttr(attrMutates) {
353+
if base.Flag.LowerM >= 1 {
354+
base.WarnfAt(n.Pos(), "zero-copy string->[]byte conversion")
355+
}
356+
n.SetOp(ir.OSTR2BYTESTMP)
357+
}
358+
}
340359
}
341360
}
342361

@@ -345,10 +364,10 @@ func (b *batch) finish(fns []*ir.Func) {
345364
// fn has not yet been analyzed, so its parameters and results
346365
// should be incorporated directly into the flow graph instead of
347366
// relying on its escape analysis tagging.
348-
func (e *escape) inMutualBatch(fn *ir.Name) bool {
367+
func (b *batch) inMutualBatch(fn *ir.Name) bool {
349368
if fn.Defn != nil && fn.Defn.Esc() < escFuncTagged {
350369
if fn.Defn.Esc() == escFuncUnknown {
351-
base.Fatalf("graph inconsistency: %v", fn)
370+
base.FatalfAt(fn.Pos(), "graph inconsistency: %v", fn)
352371
}
353372
return true
354373
}
@@ -411,6 +430,8 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
411430
if diagnose && f.Sym != nil {
412431
base.WarnfAt(f.Pos, "%v does not escape", name())
413432
}
433+
esc.AddMutator(0)
434+
esc.AddCallee(0)
414435
} else {
415436
if diagnose && f.Sym != nil {
416437
base.WarnfAt(f.Pos, "leaking param: %v", name())
@@ -453,21 +474,37 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
453474
esc.Optimize()
454475

455476
if diagnose && !loc.hasAttr(attrEscapes) {
456-
if esc.Empty() {
457-
base.WarnfAt(f.Pos, "%v does not escape", name())
458-
}
477+
anyLeaks := false
459478
if x := esc.Heap(); x >= 0 {
460479
if x == 0 {
461480
base.WarnfAt(f.Pos, "leaking param: %v", name())
462481
} else {
463482
// TODO(mdempsky): Mention level=x like below?
464483
base.WarnfAt(f.Pos, "leaking param content: %v", name())
465484
}
485+
anyLeaks = true
466486
}
467487
for i := 0; i < numEscResults; i++ {
468488
if x := esc.Result(i); x >= 0 {
469489
res := fn.Type().Results().Field(i).Sym
470490
base.WarnfAt(f.Pos, "leaking param: %v to result %v level=%d", name(), res, x)
491+
anyLeaks = true
492+
}
493+
}
494+
if !anyLeaks {
495+
base.WarnfAt(f.Pos, "%v does not escape", name())
496+
}
497+
498+
if base.Flag.LowerM >= 2 {
499+
if x := esc.Mutator(); x >= 0 {
500+
base.WarnfAt(f.Pos, "mutates param: %v derefs=%v", name(), x)
501+
} else {
502+
base.WarnfAt(f.Pos, "does not mutate param: %v", name())
503+
}
504+
if x := esc.Callee(); x >= 0 {
505+
base.WarnfAt(f.Pos, "calls param: %v derefs=%v", name(), x)
506+
} else {
507+
base.WarnfAt(f.Pos, "does not call param: %v", name())
471508
}
472509
}
473510
}

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

+47-5
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ const (
8888
// address outlives the statement; that is, whether its storage
8989
// cannot be immediately reused.
9090
attrPersists
91+
92+
// attrMutates indicates whether pointers that are reachable from
93+
// this location may have their addressed memory mutated. This is
94+
// used to detect string->[]byte conversions that can be safely
95+
// optimized away.
96+
attrMutates
97+
98+
// attrCalls indicates whether closures that are reachable from this
99+
// location may be called without tracking their results. This is
100+
// used to better optimize indirect closure calls.
101+
attrCalls
91102
)
92103

93104
func (l *location) hasAttr(attr locAttr) bool { return l.attrs&attr != 0 }
@@ -121,6 +132,35 @@ func (l *location) leakTo(sink *location, derefs int) {
121132
l.paramEsc.AddHeap(derefs)
122133
}
123134

135+
// leakTo records that parameter l leaks to sink.
136+
func (b *batch) leakTo(l, sink *location, derefs int) {
137+
if (logopt.Enabled() || base.Flag.LowerM >= 2) && !l.hasAttr(attrEscapes) {
138+
if base.Flag.LowerM >= 2 {
139+
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", base.FmtPos(l.n.Pos()), l.n, b.explainLoc(sink), derefs)
140+
}
141+
explanation := b.explainPath(sink, l)
142+
if logopt.Enabled() {
143+
var e_curfn *ir.Func // TODO(mdempsky): Fix.
144+
logopt.LogOpt(l.n.Pos(), "leak", "escape", ir.FuncName(e_curfn),
145+
fmt.Sprintf("parameter %v leaks to %s with derefs=%d", l.n, b.explainLoc(sink), derefs), explanation)
146+
}
147+
}
148+
149+
// If sink is a result parameter that doesn't escape (#44614)
150+
// and we can fit return bits into the escape analysis tag,
151+
// then record as a result leak.
152+
if !sink.hasAttr(attrEscapes) && sink.isName(ir.PPARAMOUT) && sink.curfn == l.curfn {
153+
if ri := sink.resultIndex - 1; ri < numEscResults {
154+
// Leak to result parameter.
155+
l.paramEsc.AddResult(ri, derefs)
156+
return
157+
}
158+
}
159+
160+
// Otherwise, record as heap leak.
161+
l.paramEsc.AddHeap(derefs)
162+
}
163+
124164
func (l *location) isName(c ir.Class) bool {
125165
return l.n != nil && l.n.Op() == ir.ONAME && l.n.(*ir.Name).Class == c
126166
}
@@ -203,7 +243,7 @@ func (b *batch) flow(k hole, src *location) {
203243
}
204244

205245
}
206-
src.attrs |= attrEscapes
246+
src.attrs |= attrEscapes | attrPersists | attrMutates | attrCalls
207247
return
208248
}
209249

@@ -212,11 +252,13 @@ func (b *batch) flow(k hole, src *location) {
212252
}
213253

214254
func (b *batch) heapHole() hole { return b.heapLoc.asHole() }
255+
func (b *batch) mutatorHole() hole { return b.mutatorLoc.asHole() }
256+
func (b *batch) calleeHole() hole { return b.calleeLoc.asHole() }
215257
func (b *batch) discardHole() hole { return b.blankLoc.asHole() }
216258

217259
func (b *batch) oldLoc(n *ir.Name) *location {
218260
if n.Canonical().Opt == nil {
219-
base.Fatalf("%v has no location", n)
261+
base.FatalfAt(n.Pos(), "%v has no location", n)
220262
}
221263
return n.Canonical().Opt.(*location)
222264
}
@@ -231,7 +273,7 @@ func (e *escape) newLoc(n ir.Node, persists bool) *location {
231273

232274
if n != nil && n.Op() == ir.ONAME {
233275
if canon := n.(*ir.Name).Canonical(); n != canon {
234-
base.Fatalf("newLoc on non-canonical %v (canonical is %v)", n, canon)
276+
base.FatalfAt(n.Pos(), "newLoc on non-canonical %v (canonical is %v)", n, canon)
235277
}
236278
}
237279
loc := &location{
@@ -249,11 +291,11 @@ func (e *escape) newLoc(n ir.Node, persists bool) *location {
249291
if n.Class == ir.PPARAM && n.Curfn == nil {
250292
// ok; hidden parameter
251293
} else if n.Curfn != e.curfn {
252-
base.Fatalf("curfn mismatch: %v != %v for %v", n.Curfn, e.curfn, n)
294+
base.FatalfAt(n.Pos(), "curfn mismatch: %v != %v for %v", n.Curfn, e.curfn, n)
253295
}
254296

255297
if n.Opt != nil {
256-
base.Fatalf("%v already has a location", n)
298+
base.FatalfAt(n.Pos(), "%v already has a location", n)
257299
}
258300
n.Opt = loc
259301
}

0 commit comments

Comments
 (0)