mirror of
https://github.com/golang/go
synced 2024-10-02 22:25:08 +00:00
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 <mdempsky@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
This commit is contained in:
parent
f278ae61d5
commit
ff47dd1d66
|
@ -39,10 +39,10 @@ func (e *escape) addr(n ir.Node) hole {
|
||||||
if n.X.Type().IsArray() {
|
if n.X.Type().IsArray() {
|
||||||
k = e.addr(n.X)
|
k = e.addr(n.X)
|
||||||
} else {
|
} else {
|
||||||
e.discard(n.X)
|
e.mutate(n.X)
|
||||||
}
|
}
|
||||||
case ir.ODEREF, ir.ODOTPTR:
|
case ir.ODEREF, ir.ODOTPTR:
|
||||||
e.discard(n)
|
e.mutate(n)
|
||||||
case ir.OINDEXMAP:
|
case ir.OINDEXMAP:
|
||||||
n := n.(*ir.IndexExpr)
|
n := n.(*ir.IndexExpr)
|
||||||
e.discard(n.X)
|
e.discard(n.X)
|
||||||
|
@ -52,6 +52,10 @@ func (e *escape) addr(n ir.Node) hole {
|
||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *escape) mutate(n ir.Node) {
|
||||||
|
e.expr(e.mutatorHole(), n)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *escape) addrs(l ir.Nodes) []hole {
|
func (e *escape) addrs(l ir.Nodes) []hole {
|
||||||
var ks []hole
|
var ks []hole
|
||||||
for _, n := range l {
|
for _, n := range l {
|
||||||
|
|
|
@ -68,17 +68,8 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
|
||||||
var fn *ir.Name
|
var fn *ir.Name
|
||||||
switch call.Op() {
|
switch call.Op() {
|
||||||
case ir.OCALLFUNC:
|
case ir.OCALLFUNC:
|
||||||
// If we have a direct call to a closure (not just one we were
|
|
||||||
// able to statically resolve with ir.StaticValue), mark it as
|
|
||||||
// such so batch.outlives can optimize the flow results.
|
|
||||||
if call.X.Op() == ir.OCLOSURE {
|
|
||||||
call.X.(*ir.ClosureExpr).Func.SetClosureCalled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
v := ir.StaticValue(call.X)
|
v := ir.StaticValue(call.X)
|
||||||
fn = ir.StaticCalleeName(v)
|
fn = ir.StaticCalleeName(v)
|
||||||
case ir.OCALLMETH:
|
|
||||||
base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fntype := call.X.Type()
|
fntype := call.X.Type()
|
||||||
|
@ -88,7 +79,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
|
||||||
|
|
||||||
if ks != nil && fn != nil && e.inMutualBatch(fn) {
|
if ks != nil && fn != nil && e.inMutualBatch(fn) {
|
||||||
for i, result := range fn.Type().Results().FieldSlice() {
|
for i, result := range fn.Type().Results().FieldSlice() {
|
||||||
e.expr(ks[i], ir.AsNode(result.Nname))
|
e.expr(ks[i], result.Nname.(*ir.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +90,20 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
|
||||||
// Note: We use argument and not argumentFunc, because while
|
// Note: We use argument and not argumentFunc, because while
|
||||||
// call.X here may be an argument to runtime.{new,defer}proc,
|
// call.X here may be an argument to runtime.{new,defer}proc,
|
||||||
// it's not an argument to fn itself.
|
// it's not an argument to fn itself.
|
||||||
argument(e.discardHole(), &call.X)
|
calleeK := e.discardHole()
|
||||||
|
if fn == nil { // unknown callee
|
||||||
|
for _, k := range ks {
|
||||||
|
if k.dst != &e.blankLoc {
|
||||||
|
// The results flow somewhere, but we don't statically
|
||||||
|
// know the callee function. If a closure flows here, we
|
||||||
|
// need to conservatively assume its results might flow to
|
||||||
|
// the heap.
|
||||||
|
calleeK = e.calleeHole()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argument(calleeK, &call.X)
|
||||||
} else {
|
} else {
|
||||||
recvp = &call.X.(*ir.SelectorExpr).X
|
recvp = &call.X.(*ir.SelectorExpr).X
|
||||||
}
|
}
|
||||||
|
@ -139,7 +143,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
|
||||||
// it has enough capacity. Alternatively, a new heap
|
// it has enough capacity. Alternatively, a new heap
|
||||||
// slice might be allocated, and all slice elements
|
// slice might be allocated, and all slice elements
|
||||||
// might flow to heap.
|
// might flow to heap.
|
||||||
appendeeK := ks[0]
|
appendeeK := e.teeHole(ks[0], e.mutatorHole())
|
||||||
if args[0].Type().Elem().HasPointers() {
|
if args[0].Type().Elem().HasPointers() {
|
||||||
appendeeK = e.teeHole(appendeeK, e.heapHole().deref(call, "appendee slice"))
|
appendeeK = e.teeHole(appendeeK, e.heapHole().deref(call, "appendee slice"))
|
||||||
}
|
}
|
||||||
|
@ -160,7 +164,7 @@ func (e *escape) callCommon(ks []hole, call ir.Node, init *ir.Nodes, wrapper *ir
|
||||||
|
|
||||||
case ir.OCOPY:
|
case ir.OCOPY:
|
||||||
call := call.(*ir.BinaryExpr)
|
call := call.(*ir.BinaryExpr)
|
||||||
argument(e.discardHole(), &call.X)
|
argument(e.mutatorHole(), &call.X)
|
||||||
|
|
||||||
copiedK := e.discardHole()
|
copiedK := e.discardHole()
|
||||||
if call.Y.Type().IsSlice() && call.Y.Type().Elem().HasPointers() {
|
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
|
||||||
}
|
}
|
||||||
argumentRType(&call.RType)
|
argumentRType(&call.RType)
|
||||||
|
|
||||||
case ir.OLEN, ir.OCAP, ir.OREAL, ir.OIMAG, ir.OCLOSE, ir.OCLEAR:
|
case ir.OLEN, ir.OCAP, ir.OREAL, ir.OIMAG, ir.OCLOSE:
|
||||||
call := call.(*ir.UnaryExpr)
|
call := call.(*ir.UnaryExpr)
|
||||||
argument(e.discardHole(), &call.X)
|
argument(e.discardHole(), &call.X)
|
||||||
|
|
||||||
|
case ir.OCLEAR:
|
||||||
|
call := call.(*ir.UnaryExpr)
|
||||||
|
argument(e.mutatorHole(), &call.X)
|
||||||
|
|
||||||
case ir.OUNSAFESTRINGDATA, ir.OUNSAFESLICEDATA:
|
case ir.OUNSAFESTRINGDATA, ir.OUNSAFESLICEDATA:
|
||||||
call := call.(*ir.UnaryExpr)
|
call := call.(*ir.UnaryExpr)
|
||||||
argument(ks[0], &call.X)
|
argument(ks[0], &call.X)
|
||||||
|
@ -251,6 +259,7 @@ func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
|
||||||
fn := ir.NewClosureFunc(n.Pos(), true)
|
fn := ir.NewClosureFunc(n.Pos(), true)
|
||||||
fn.SetWrapper(true)
|
fn.SetWrapper(true)
|
||||||
fn.Nname.SetType(types.NewSignature(nil, nil, nil))
|
fn.Nname.SetType(types.NewSignature(nil, nil, nil))
|
||||||
|
fn.SetEsc(escFuncTagged) // no params; effectively tagged already
|
||||||
fn.Body = []ir.Node{call}
|
fn.Body = []ir.Node{call}
|
||||||
if call, ok := call.(*ir.CallExpr); ok && call.Op() == ir.OCALLFUNC {
|
if call, ok := call.(*ir.CallExpr); ok && call.Op() == ir.OCALLFUNC {
|
||||||
// If the callee is a named function, link to the original callee.
|
// 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
|
||||||
// Create and declare a new pointer-typed temp variable.
|
// Create and declare a new pointer-typed temp variable.
|
||||||
tmp := e.wrapExpr(arg.Pos(), &arg.X, init, call, wrapper)
|
tmp := e.wrapExpr(arg.Pos(), &arg.X, init, call, wrapper)
|
||||||
|
|
||||||
|
k := e.mutatorHole()
|
||||||
if pragma&ir.UintptrEscapes != 0 {
|
if pragma&ir.UintptrEscapes != 0 {
|
||||||
e.flow(e.heapHole().note(arg, "//go:uintptrescapes"), e.oldLoc(tmp))
|
k = e.heapHole().note(arg, "//go:uintptrescapes")
|
||||||
}
|
}
|
||||||
|
e.flow(k, e.oldLoc(tmp))
|
||||||
|
|
||||||
if pragma&ir.UintptrKeepAlive != 0 {
|
if pragma&ir.UintptrKeepAlive != 0 {
|
||||||
call := call.(*ir.CallExpr)
|
call := call.(*ir.CallExpr)
|
||||||
|
@ -454,11 +465,17 @@ func (e *escape) tagHole(ks []hole, fn *ir.Name, param *types.Field) hole {
|
||||||
// Call to previously tagged function.
|
// Call to previously tagged function.
|
||||||
|
|
||||||
var tagKs []hole
|
var tagKs []hole
|
||||||
|
|
||||||
esc := parseLeaks(param.Note)
|
esc := parseLeaks(param.Note)
|
||||||
|
|
||||||
if x := esc.Heap(); x >= 0 {
|
if x := esc.Heap(); x >= 0 {
|
||||||
tagKs = append(tagKs, e.heapHole().shift(x))
|
tagKs = append(tagKs, e.heapHole().shift(x))
|
||||||
}
|
}
|
||||||
|
if x := esc.Mutator(); x >= 0 {
|
||||||
|
tagKs = append(tagKs, e.mutatorHole().shift(x))
|
||||||
|
}
|
||||||
|
if x := esc.Callee(); x >= 0 {
|
||||||
|
tagKs = append(tagKs, e.calleeHole().shift(x))
|
||||||
|
}
|
||||||
|
|
||||||
if ks != nil {
|
if ks != nil {
|
||||||
for i := 0; i < numEscResults; i++ {
|
for i := 0; i < numEscResults; i++ {
|
||||||
|
|
|
@ -88,8 +88,10 @@ type batch struct {
|
||||||
allLocs []*location
|
allLocs []*location
|
||||||
closures []closure
|
closures []closure
|
||||||
|
|
||||||
heapLoc location
|
heapLoc location
|
||||||
blankLoc location
|
mutatorLoc location
|
||||||
|
calleeLoc location
|
||||||
|
blankLoc location
|
||||||
}
|
}
|
||||||
|
|
||||||
// A closure holds a closure expression and its spill hole (i.e.,
|
// A closure holds a closure expression and its spill hole (i.e.,
|
||||||
|
@ -129,7 +131,9 @@ func Batch(fns []*ir.Func, recursive bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var b batch
|
var b batch
|
||||||
b.heapLoc.attrs = attrEscapes | attrPersists
|
b.heapLoc.attrs = attrEscapes | attrPersists | attrMutates | attrCalls
|
||||||
|
b.mutatorLoc.attrs = attrMutates
|
||||||
|
b.calleeLoc.attrs = attrCalls
|
||||||
|
|
||||||
// Construct data-flow graph from syntax trees.
|
// Construct data-flow graph from syntax trees.
|
||||||
for _, fn := range fns {
|
for _, fn := range fns {
|
||||||
|
@ -288,6 +292,7 @@ func (b *batch) finish(fns []*ir.Func) {
|
||||||
if n == nil {
|
if n == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.Op() == ir.ONAME {
|
if n.Op() == ir.ONAME {
|
||||||
n := n.(*ir.Name)
|
n := n.(*ir.Name)
|
||||||
n.Opt = nil
|
n.Opt = nil
|
||||||
|
@ -337,6 +342,20 @@ func (b *batch) finish(fns []*ir.Func) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the result of a string->[]byte conversion is never mutated,
|
||||||
|
// then it can simply reuse the string's memory directly.
|
||||||
|
//
|
||||||
|
// TODO(mdempsky): Enable in a subsequent CL. We need to ensure
|
||||||
|
// []byte("") evaluates to []byte{}, not []byte(nil).
|
||||||
|
if false {
|
||||||
|
if n, ok := n.(*ir.ConvExpr); ok && n.Op() == ir.OSTR2BYTES && !loc.hasAttr(attrMutates) {
|
||||||
|
if base.Flag.LowerM >= 1 {
|
||||||
|
base.WarnfAt(n.Pos(), "zero-copy string->[]byte conversion")
|
||||||
|
}
|
||||||
|
n.SetOp(ir.OSTR2BYTESTMP)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,10 +364,10 @@ func (b *batch) finish(fns []*ir.Func) {
|
||||||
// fn has not yet been analyzed, so its parameters and results
|
// fn has not yet been analyzed, so its parameters and results
|
||||||
// should be incorporated directly into the flow graph instead of
|
// should be incorporated directly into the flow graph instead of
|
||||||
// relying on its escape analysis tagging.
|
// relying on its escape analysis tagging.
|
||||||
func (e *escape) inMutualBatch(fn *ir.Name) bool {
|
func (b *batch) inMutualBatch(fn *ir.Name) bool {
|
||||||
if fn.Defn != nil && fn.Defn.Esc() < escFuncTagged {
|
if fn.Defn != nil && fn.Defn.Esc() < escFuncTagged {
|
||||||
if fn.Defn.Esc() == escFuncUnknown {
|
if fn.Defn.Esc() == escFuncUnknown {
|
||||||
base.Fatalf("graph inconsistency: %v", fn)
|
base.FatalfAt(fn.Pos(), "graph inconsistency: %v", fn)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -411,6 +430,8 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
|
||||||
if diagnose && f.Sym != nil {
|
if diagnose && f.Sym != nil {
|
||||||
base.WarnfAt(f.Pos, "%v does not escape", name())
|
base.WarnfAt(f.Pos, "%v does not escape", name())
|
||||||
}
|
}
|
||||||
|
esc.AddMutator(0)
|
||||||
|
esc.AddCallee(0)
|
||||||
} else {
|
} else {
|
||||||
if diagnose && f.Sym != nil {
|
if diagnose && f.Sym != nil {
|
||||||
base.WarnfAt(f.Pos, "leaking param: %v", name())
|
base.WarnfAt(f.Pos, "leaking param: %v", name())
|
||||||
|
@ -453,9 +474,7 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
|
||||||
esc.Optimize()
|
esc.Optimize()
|
||||||
|
|
||||||
if diagnose && !loc.hasAttr(attrEscapes) {
|
if diagnose && !loc.hasAttr(attrEscapes) {
|
||||||
if esc.Empty() {
|
anyLeaks := false
|
||||||
base.WarnfAt(f.Pos, "%v does not escape", name())
|
|
||||||
}
|
|
||||||
if x := esc.Heap(); x >= 0 {
|
if x := esc.Heap(); x >= 0 {
|
||||||
if x == 0 {
|
if x == 0 {
|
||||||
base.WarnfAt(f.Pos, "leaking param: %v", name())
|
base.WarnfAt(f.Pos, "leaking param: %v", name())
|
||||||
|
@ -463,11 +482,29 @@ func (b *batch) paramTag(fn *ir.Func, narg int, f *types.Field) string {
|
||||||
// TODO(mdempsky): Mention level=x like below?
|
// TODO(mdempsky): Mention level=x like below?
|
||||||
base.WarnfAt(f.Pos, "leaking param content: %v", name())
|
base.WarnfAt(f.Pos, "leaking param content: %v", name())
|
||||||
}
|
}
|
||||||
|
anyLeaks = true
|
||||||
}
|
}
|
||||||
for i := 0; i < numEscResults; i++ {
|
for i := 0; i < numEscResults; i++ {
|
||||||
if x := esc.Result(i); x >= 0 {
|
if x := esc.Result(i); x >= 0 {
|
||||||
res := fn.Type().Results().Field(i).Sym
|
res := fn.Type().Results().Field(i).Sym
|
||||||
base.WarnfAt(f.Pos, "leaking param: %v to result %v level=%d", name(), res, x)
|
base.WarnfAt(f.Pos, "leaking param: %v to result %v level=%d", name(), res, x)
|
||||||
|
anyLeaks = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyLeaks {
|
||||||
|
base.WarnfAt(f.Pos, "%v does not escape", name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if base.Flag.LowerM >= 2 {
|
||||||
|
if x := esc.Mutator(); x >= 0 {
|
||||||
|
base.WarnfAt(f.Pos, "mutates param: %v derefs=%v", name(), x)
|
||||||
|
} else {
|
||||||
|
base.WarnfAt(f.Pos, "does not mutate param: %v", name())
|
||||||
|
}
|
||||||
|
if x := esc.Callee(); x >= 0 {
|
||||||
|
base.WarnfAt(f.Pos, "calls param: %v derefs=%v", name(), x)
|
||||||
|
} else {
|
||||||
|
base.WarnfAt(f.Pos, "does not call param: %v", name())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,17 @@ const (
|
||||||
// address outlives the statement; that is, whether its storage
|
// address outlives the statement; that is, whether its storage
|
||||||
// cannot be immediately reused.
|
// cannot be immediately reused.
|
||||||
attrPersists
|
attrPersists
|
||||||
|
|
||||||
|
// attrMutates indicates whether pointers that are reachable from
|
||||||
|
// this location may have their addressed memory mutated. This is
|
||||||
|
// used to detect string->[]byte conversions that can be safely
|
||||||
|
// optimized away.
|
||||||
|
attrMutates
|
||||||
|
|
||||||
|
// attrCalls indicates whether closures that are reachable from this
|
||||||
|
// location may be called without tracking their results. This is
|
||||||
|
// used to better optimize indirect closure calls.
|
||||||
|
attrCalls
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l *location) hasAttr(attr locAttr) bool { return l.attrs&attr != 0 }
|
func (l *location) hasAttr(attr locAttr) bool { return l.attrs&attr != 0 }
|
||||||
|
@ -121,6 +132,35 @@ func (l *location) leakTo(sink *location, derefs int) {
|
||||||
l.paramEsc.AddHeap(derefs)
|
l.paramEsc.AddHeap(derefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// leakTo records that parameter l leaks to sink.
|
||||||
|
func (b *batch) leakTo(l, sink *location, derefs int) {
|
||||||
|
if (logopt.Enabled() || base.Flag.LowerM >= 2) && !l.hasAttr(attrEscapes) {
|
||||||
|
if base.Flag.LowerM >= 2 {
|
||||||
|
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", base.FmtPos(l.n.Pos()), l.n, b.explainLoc(sink), derefs)
|
||||||
|
}
|
||||||
|
explanation := b.explainPath(sink, l)
|
||||||
|
if logopt.Enabled() {
|
||||||
|
var e_curfn *ir.Func // TODO(mdempsky): Fix.
|
||||||
|
logopt.LogOpt(l.n.Pos(), "leak", "escape", ir.FuncName(e_curfn),
|
||||||
|
fmt.Sprintf("parameter %v leaks to %s with derefs=%d", l.n, b.explainLoc(sink), derefs), explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If sink is a result parameter that doesn't escape (#44614)
|
||||||
|
// and we can fit return bits into the escape analysis tag,
|
||||||
|
// then record as a result leak.
|
||||||
|
if !sink.hasAttr(attrEscapes) && sink.isName(ir.PPARAMOUT) && sink.curfn == l.curfn {
|
||||||
|
if ri := sink.resultIndex - 1; ri < numEscResults {
|
||||||
|
// Leak to result parameter.
|
||||||
|
l.paramEsc.AddResult(ri, derefs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, record as heap leak.
|
||||||
|
l.paramEsc.AddHeap(derefs)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *location) isName(c ir.Class) bool {
|
func (l *location) isName(c ir.Class) bool {
|
||||||
return l.n != nil && l.n.Op() == ir.ONAME && l.n.(*ir.Name).Class == c
|
return l.n != nil && l.n.Op() == ir.ONAME && l.n.(*ir.Name).Class == c
|
||||||
}
|
}
|
||||||
|
@ -203,7 +243,7 @@ func (b *batch) flow(k hole, src *location) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
src.attrs |= attrEscapes
|
src.attrs |= attrEscapes | attrPersists | attrMutates | attrCalls
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,11 +252,13 @@ func (b *batch) flow(k hole, src *location) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *batch) heapHole() hole { return b.heapLoc.asHole() }
|
func (b *batch) heapHole() hole { return b.heapLoc.asHole() }
|
||||||
|
func (b *batch) mutatorHole() hole { return b.mutatorLoc.asHole() }
|
||||||
|
func (b *batch) calleeHole() hole { return b.calleeLoc.asHole() }
|
||||||
func (b *batch) discardHole() hole { return b.blankLoc.asHole() }
|
func (b *batch) discardHole() hole { return b.blankLoc.asHole() }
|
||||||
|
|
||||||
func (b *batch) oldLoc(n *ir.Name) *location {
|
func (b *batch) oldLoc(n *ir.Name) *location {
|
||||||
if n.Canonical().Opt == nil {
|
if n.Canonical().Opt == nil {
|
||||||
base.Fatalf("%v has no location", n)
|
base.FatalfAt(n.Pos(), "%v has no location", n)
|
||||||
}
|
}
|
||||||
return n.Canonical().Opt.(*location)
|
return n.Canonical().Opt.(*location)
|
||||||
}
|
}
|
||||||
|
@ -231,7 +273,7 @@ func (e *escape) newLoc(n ir.Node, persists bool) *location {
|
||||||
|
|
||||||
if n != nil && n.Op() == ir.ONAME {
|
if n != nil && n.Op() == ir.ONAME {
|
||||||
if canon := n.(*ir.Name).Canonical(); n != canon {
|
if canon := n.(*ir.Name).Canonical(); n != canon {
|
||||||
base.Fatalf("newLoc on non-canonical %v (canonical is %v)", n, canon)
|
base.FatalfAt(n.Pos(), "newLoc on non-canonical %v (canonical is %v)", n, canon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loc := &location{
|
loc := &location{
|
||||||
|
@ -249,11 +291,11 @@ func (e *escape) newLoc(n ir.Node, persists bool) *location {
|
||||||
if n.Class == ir.PPARAM && n.Curfn == nil {
|
if n.Class == ir.PPARAM && n.Curfn == nil {
|
||||||
// ok; hidden parameter
|
// ok; hidden parameter
|
||||||
} else if n.Curfn != e.curfn {
|
} else if n.Curfn != e.curfn {
|
||||||
base.Fatalf("curfn mismatch: %v != %v for %v", n.Curfn, e.curfn, n)
|
base.FatalfAt(n.Pos(), "curfn mismatch: %v != %v for %v", n.Curfn, e.curfn, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n.Opt != nil {
|
if n.Opt != nil {
|
||||||
base.Fatalf("%v already has a location", n)
|
base.FatalfAt(n.Pos(), "%v already has a location", n)
|
||||||
}
|
}
|
||||||
n.Opt = loc
|
n.Opt = loc
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,33 +10,53 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const numEscResults = 7
|
// A leaks represents a set of assignment flows from a parameter to
|
||||||
|
// the heap, mutator, callee, or to any of its function's (first
|
||||||
|
// numEscResults) result parameters.
|
||||||
|
type leaks [8]uint8
|
||||||
|
|
||||||
// An leaks represents a set of assignment flows from a parameter
|
const (
|
||||||
// to the heap or to any of its function's (first numEscResults)
|
leakHeap = iota
|
||||||
// result parameters.
|
leakMutator
|
||||||
type leaks [1 + numEscResults]uint8
|
leakCallee
|
||||||
|
leakResult0
|
||||||
|
)
|
||||||
|
|
||||||
// Empty reports whether l is an empty set (i.e., no assignment flows).
|
const numEscResults = len(leaks{}) - leakResult0
|
||||||
func (l leaks) Empty() bool { return l == leaks{} }
|
|
||||||
|
|
||||||
// Heap returns the minimum deref count of any assignment flow from l
|
// Heap returns the minimum deref count of any assignment flow from l
|
||||||
// to the heap. If no such flows exist, Heap returns -1.
|
// to the heap. If no such flows exist, Heap returns -1.
|
||||||
func (l leaks) Heap() int { return l.get(0) }
|
func (l leaks) Heap() int { return l.get(leakHeap) }
|
||||||
|
|
||||||
|
// Mutator returns the minimum deref count of any assignment flow from
|
||||||
|
// l to the pointer operand of an indirect assignment statement. If no
|
||||||
|
// such flows exist, Mutator returns -1.
|
||||||
|
func (l leaks) Mutator() int { return l.get(leakMutator) }
|
||||||
|
|
||||||
|
// Callee returns the minimum deref count of any assignment flow from
|
||||||
|
// l to the callee operand of call expression. If no such flows exist,
|
||||||
|
// Callee returns -1.
|
||||||
|
func (l leaks) Callee() int { return l.get(leakCallee) }
|
||||||
|
|
||||||
// Result returns the minimum deref count of any assignment flow from
|
// Result returns the minimum deref count of any assignment flow from
|
||||||
// l to its function's i'th result parameter. If no such flows exist,
|
// l to its function's i'th result parameter. If no such flows exist,
|
||||||
// Result returns -1.
|
// Result returns -1.
|
||||||
func (l leaks) Result(i int) int { return l.get(1 + i) }
|
func (l leaks) Result(i int) int { return l.get(leakResult0 + i) }
|
||||||
|
|
||||||
// AddHeap adds an assignment flow from l to the heap.
|
// AddHeap adds an assignment flow from l to the heap.
|
||||||
func (l *leaks) AddHeap(derefs int) { l.add(0, derefs) }
|
func (l *leaks) AddHeap(derefs int) { l.add(leakHeap, derefs) }
|
||||||
|
|
||||||
|
// AddMutator adds a flow from l to the mutator (i.e., a pointer
|
||||||
|
// operand of an indirect assignment statement).
|
||||||
|
func (l *leaks) AddMutator(derefs int) { l.add(leakMutator, derefs) }
|
||||||
|
|
||||||
|
// AddCallee adds an assignment flow from l to the callee operand of a
|
||||||
|
// call expression.
|
||||||
|
func (l *leaks) AddCallee(derefs int) { l.add(leakCallee, derefs) }
|
||||||
|
|
||||||
// AddResult adds an assignment flow from l to its function's i'th
|
// AddResult adds an assignment flow from l to its function's i'th
|
||||||
// result parameter.
|
// result parameter.
|
||||||
func (l *leaks) AddResult(i, derefs int) { l.add(1+i, derefs) }
|
func (l *leaks) AddResult(i, derefs int) { l.add(leakResult0+i, derefs) }
|
||||||
|
|
||||||
func (l *leaks) setResult(i, derefs int) { l.set(1+i, derefs) }
|
|
||||||
|
|
||||||
func (l leaks) get(i int) int { return int(l[i]) - 1 }
|
func (l leaks) get(i int) int { return int(l[i]) - 1 }
|
||||||
|
|
||||||
|
@ -64,9 +84,9 @@ func (l *leaks) Optimize() {
|
||||||
// If we have a path to the heap, then there's no use in
|
// If we have a path to the heap, then there's no use in
|
||||||
// keeping equal or longer paths elsewhere.
|
// keeping equal or longer paths elsewhere.
|
||||||
if x := l.Heap(); x >= 0 {
|
if x := l.Heap(); x >= 0 {
|
||||||
for i := 0; i < numEscResults; i++ {
|
for i := 1; i < len(*l); i++ {
|
||||||
if l.Result(i) >= x {
|
if l.get(i) >= x {
|
||||||
l.setResult(i, -1)
|
l.set(i, -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@ func (b *batch) walkAll() {
|
||||||
for _, loc := range b.allLocs {
|
for _, loc := range b.allLocs {
|
||||||
enqueue(loc)
|
enqueue(loc)
|
||||||
}
|
}
|
||||||
|
enqueue(&b.mutatorLoc)
|
||||||
|
enqueue(&b.calleeLoc)
|
||||||
enqueue(&b.heapLoc)
|
enqueue(&b.heapLoc)
|
||||||
|
|
||||||
var walkgen uint32
|
var walkgen uint32
|
||||||
|
@ -61,12 +63,27 @@ func (b *batch) walkOne(root *location, walkgen uint32, enqueue func(*location))
|
||||||
root.derefs = 0
|
root.derefs = 0
|
||||||
root.dst = nil
|
root.dst = nil
|
||||||
|
|
||||||
|
if root.hasAttr(attrCalls) {
|
||||||
|
if clo, ok := root.n.(*ir.ClosureExpr); ok {
|
||||||
|
if fn := clo.Func; b.inMutualBatch(fn.Nname) && !fn.ClosureResultsLost() {
|
||||||
|
fn.SetClosureResultsLost(true)
|
||||||
|
|
||||||
|
// Re-flow from the closure's results, now that we're aware
|
||||||
|
// we lost track of them.
|
||||||
|
for _, result := range fn.Type().Results().FieldSlice() {
|
||||||
|
enqueue(b.oldLoc(result.Nname.(*ir.Name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
todo := []*location{root} // LIFO queue
|
todo := []*location{root} // LIFO queue
|
||||||
for len(todo) > 0 {
|
for len(todo) > 0 {
|
||||||
l := todo[len(todo)-1]
|
l := todo[len(todo)-1]
|
||||||
todo = todo[:len(todo)-1]
|
todo = todo[:len(todo)-1]
|
||||||
|
|
||||||
derefs := l.derefs
|
derefs := l.derefs
|
||||||
|
var newAttrs locAttr
|
||||||
|
|
||||||
// If l.derefs < 0, then l's address flows to root.
|
// If l.derefs < 0, then l's address flows to root.
|
||||||
addressOf := derefs < 0
|
addressOf := derefs < 0
|
||||||
|
@ -77,22 +94,41 @@ func (b *batch) walkOne(root *location, walkgen uint32, enqueue func(*location))
|
||||||
// derefs at 0.
|
// derefs at 0.
|
||||||
derefs = 0
|
derefs = 0
|
||||||
|
|
||||||
|
// If l's address flows somewhere that
|
||||||
|
// outlives it, then l needs to be heap
|
||||||
|
// allocated.
|
||||||
|
if b.outlives(root, l) {
|
||||||
|
if !l.hasAttr(attrEscapes) && (logopt.Enabled() || base.Flag.LowerM >= 2) {
|
||||||
|
if base.Flag.LowerM >= 2 {
|
||||||
|
fmt.Printf("%s: %v escapes to heap:\n", base.FmtPos(l.n.Pos()), l.n)
|
||||||
|
}
|
||||||
|
explanation := b.explainPath(root, l)
|
||||||
|
if logopt.Enabled() {
|
||||||
|
var e_curfn *ir.Func // TODO(mdempsky): Fix.
|
||||||
|
logopt.LogOpt(l.n.Pos(), "escape", "escape", ir.FuncName(e_curfn), fmt.Sprintf("%v escapes to heap", l.n), explanation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttrs |= attrEscapes | attrPersists | attrMutates | attrCalls
|
||||||
|
} else
|
||||||
// If l's address flows to a persistent location, then l needs
|
// If l's address flows to a persistent location, then l needs
|
||||||
// to persist too.
|
// to persist too.
|
||||||
if root.hasAttr(attrPersists) && !l.hasAttr(attrPersists) {
|
if root.hasAttr(attrPersists) {
|
||||||
l.attrs |= attrPersists
|
newAttrs |= attrPersists
|
||||||
enqueue(l)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.outlives(root, l) {
|
if derefs == 0 {
|
||||||
// l's value flows to root. If l is a function
|
newAttrs |= root.attrs & (attrMutates | attrCalls)
|
||||||
// parameter and root is the heap or a
|
}
|
||||||
// corresponding result parameter, then record
|
|
||||||
// that value flow for tagging the function
|
// l's value flows to root. If l is a function
|
||||||
// later.
|
// parameter and root is the heap or a
|
||||||
if l.isName(ir.PPARAM) {
|
// corresponding result parameter, then record
|
||||||
if (logopt.Enabled() || base.Flag.LowerM >= 2) && !l.hasAttr(attrEscapes) {
|
// that value flow for tagging the function
|
||||||
|
// later.
|
||||||
|
if l.isName(ir.PPARAM) {
|
||||||
|
if b.outlives(root, l) {
|
||||||
|
if !l.hasAttr(attrEscapes) && (logopt.Enabled() || base.Flag.LowerM >= 2) {
|
||||||
if base.Flag.LowerM >= 2 {
|
if base.Flag.LowerM >= 2 {
|
||||||
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", base.FmtPos(l.n.Pos()), l.n, b.explainLoc(root), derefs)
|
fmt.Printf("%s: parameter %v leaks to %s with derefs=%d:\n", base.FmtPos(l.n.Pos()), l.n, b.explainLoc(root), derefs)
|
||||||
}
|
}
|
||||||
|
@ -105,23 +141,18 @@ func (b *batch) walkOne(root *location, walkgen uint32, enqueue func(*location))
|
||||||
}
|
}
|
||||||
l.leakTo(root, derefs)
|
l.leakTo(root, derefs)
|
||||||
}
|
}
|
||||||
|
if root.hasAttr(attrMutates) {
|
||||||
|
l.paramEsc.AddMutator(derefs)
|
||||||
|
}
|
||||||
|
if root.hasAttr(attrCalls) {
|
||||||
|
l.paramEsc.AddCallee(derefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If l's address flows somewhere that
|
if newAttrs&^l.attrs != 0 {
|
||||||
// outlives it, then l needs to be heap
|
l.attrs |= newAttrs
|
||||||
// allocated.
|
enqueue(l)
|
||||||
if addressOf && !l.hasAttr(attrEscapes) {
|
if l.attrs&attrEscapes != 0 {
|
||||||
if logopt.Enabled() || base.Flag.LowerM >= 2 {
|
|
||||||
if base.Flag.LowerM >= 2 {
|
|
||||||
fmt.Printf("%s: %v escapes to heap:\n", base.FmtPos(l.n.Pos()), l.n)
|
|
||||||
}
|
|
||||||
explanation := b.explainPath(root, l)
|
|
||||||
if logopt.Enabled() {
|
|
||||||
var e_curfn *ir.Func // TODO(mdempsky): Fix.
|
|
||||||
logopt.LogOpt(l.n.Pos(), "escape", "escape", ir.FuncName(e_curfn), fmt.Sprintf("%v escapes to heap", l.n), explanation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l.attrs |= attrEscapes
|
|
||||||
enqueue(l)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,17 +262,23 @@ func (b *batch) outlives(l, other *location) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pseudo-locations that don't really exist.
|
||||||
|
if l == &b.mutatorLoc || l == &b.calleeLoc {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// We don't know what callers do with returned values, so
|
// We don't know what callers do with returned values, so
|
||||||
// pessimistically we need to assume they flow to the heap and
|
// pessimistically we need to assume they flow to the heap and
|
||||||
// outlive everything too.
|
// outlive everything too.
|
||||||
if l.isName(ir.PPARAMOUT) {
|
if l.isName(ir.PPARAMOUT) {
|
||||||
// Exception: Directly called closures can return
|
// Exception: Closures can return locations allocated outside of
|
||||||
// locations allocated outside of them without forcing
|
// them without forcing them to the heap, if we can statically
|
||||||
// them to the heap. For example:
|
// identify all call sites. For example:
|
||||||
//
|
//
|
||||||
// var u int // okay to stack allocate
|
// var u int // okay to stack allocate
|
||||||
// *(func() *int { return &u }()) = 42
|
// fn := func() *int { return &u }()
|
||||||
if containsClosure(other.curfn, l.curfn) && l.curfn.ClosureCalled() {
|
// *fn() = 42
|
||||||
|
if containsClosure(other.curfn, l.curfn) && !l.curfn.ClosureResultsLost() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,10 +289,10 @@ func (b *batch) outlives(l, other *location) bool {
|
||||||
// outlives other if it was declared outside other's loop
|
// outlives other if it was declared outside other's loop
|
||||||
// scope. For example:
|
// scope. For example:
|
||||||
//
|
//
|
||||||
// var l *int
|
// var l *int
|
||||||
// for {
|
// for {
|
||||||
// l = new(int)
|
// l = new(int) // must heap allocate: outlives for loop
|
||||||
// }
|
// }
|
||||||
if l.curfn == other.curfn && l.loopDepth < other.loopDepth {
|
if l.curfn == other.curfn && l.loopDepth < other.loopDepth {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -263,10 +300,10 @@ func (b *batch) outlives(l, other *location) bool {
|
||||||
// If other is declared within a child closure of where l is
|
// If other is declared within a child closure of where l is
|
||||||
// declared, then l outlives it. For example:
|
// declared, then l outlives it. For example:
|
||||||
//
|
//
|
||||||
// var l *int
|
// var l *int
|
||||||
// func() {
|
// func() {
|
||||||
// l = new(int)
|
// l = new(int) // must heap allocate: outlives call frame (if not inlined)
|
||||||
// }
|
// }()
|
||||||
if containsClosure(l.curfn, other.curfn) {
|
if containsClosure(l.curfn, other.curfn) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -276,8 +313,8 @@ func (b *batch) outlives(l, other *location) bool {
|
||||||
|
|
||||||
// containsClosure reports whether c is a closure contained within f.
|
// containsClosure reports whether c is a closure contained within f.
|
||||||
func containsClosure(f, c *ir.Func) bool {
|
func containsClosure(f, c *ir.Func) bool {
|
||||||
// Common case.
|
// Common cases.
|
||||||
if f == c {
|
if f == c || c.OClosure == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -215,7 +215,7 @@ const (
|
||||||
funcExportInline // include inline body in export data
|
funcExportInline // include inline body in export data
|
||||||
funcInstrumentBody // add race/msan/asan instrumentation during SSA construction
|
funcInstrumentBody // add race/msan/asan instrumentation during SSA construction
|
||||||
funcOpenCodedDeferDisallowed // can't do open-coded defers
|
funcOpenCodedDeferDisallowed // can't do open-coded defers
|
||||||
funcClosureCalled // closure is only immediately called; used by escape analysis
|
funcClosureResultsLost // closure is called indirectly and we lost track of its results; used by escape analysis
|
||||||
funcPackageInit // compiler emitted .init func for package
|
funcPackageInit // compiler emitted .init func for package
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ func (f *Func) InlinabilityChecked() bool { return f.flags&funcInlinability
|
||||||
func (f *Func) ExportInline() bool { return f.flags&funcExportInline != 0 }
|
func (f *Func) ExportInline() bool { return f.flags&funcExportInline != 0 }
|
||||||
func (f *Func) InstrumentBody() bool { return f.flags&funcInstrumentBody != 0 }
|
func (f *Func) InstrumentBody() bool { return f.flags&funcInstrumentBody != 0 }
|
||||||
func (f *Func) OpenCodedDeferDisallowed() bool { return f.flags&funcOpenCodedDeferDisallowed != 0 }
|
func (f *Func) OpenCodedDeferDisallowed() bool { return f.flags&funcOpenCodedDeferDisallowed != 0 }
|
||||||
func (f *Func) ClosureCalled() bool { return f.flags&funcClosureCalled != 0 }
|
func (f *Func) ClosureResultsLost() bool { return f.flags&funcClosureResultsLost != 0 }
|
||||||
func (f *Func) IsPackageInit() bool { return f.flags&funcPackageInit != 0 }
|
func (f *Func) IsPackageInit() bool { return f.flags&funcPackageInit != 0 }
|
||||||
|
|
||||||
func (f *Func) SetDupok(b bool) { f.flags.set(funcDupok, b) }
|
func (f *Func) SetDupok(b bool) { f.flags.set(funcDupok, b) }
|
||||||
|
@ -253,7 +253,7 @@ func (f *Func) SetInlinabilityChecked(b bool) { f.flags.set(funcInlinabilit
|
||||||
func (f *Func) SetExportInline(b bool) { f.flags.set(funcExportInline, b) }
|
func (f *Func) SetExportInline(b bool) { f.flags.set(funcExportInline, b) }
|
||||||
func (f *Func) SetInstrumentBody(b bool) { f.flags.set(funcInstrumentBody, b) }
|
func (f *Func) SetInstrumentBody(b bool) { f.flags.set(funcInstrumentBody, b) }
|
||||||
func (f *Func) SetOpenCodedDeferDisallowed(b bool) { f.flags.set(funcOpenCodedDeferDisallowed, b) }
|
func (f *Func) SetOpenCodedDeferDisallowed(b bool) { f.flags.set(funcOpenCodedDeferDisallowed, b) }
|
||||||
func (f *Func) SetClosureCalled(b bool) { f.flags.set(funcClosureCalled, b) }
|
func (f *Func) SetClosureResultsLost(b bool) { f.flags.set(funcClosureResultsLost, b) }
|
||||||
func (f *Func) SetIsPackageInit(b bool) { f.flags.set(funcPackageInit, b) }
|
func (f *Func) SetIsPackageInit(b bool) { f.flags.set(funcPackageInit, b) }
|
||||||
|
|
||||||
func (f *Func) SetWBPos(pos src.XPos) {
|
func (f *Func) SetWBPos(pos src.XPos) {
|
||||||
|
|
|
@ -177,3 +177,17 @@ func ClosureIndirect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func nopFunc(p *int) {} // ERROR "p does not escape"
|
func nopFunc(p *int) {} // ERROR "p does not escape"
|
||||||
|
|
||||||
|
func ClosureIndirect2() {
|
||||||
|
f := func(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0" "func literal does not escape"
|
||||||
|
|
||||||
|
f(new(int)) // ERROR "new\(int\) does not escape"
|
||||||
|
|
||||||
|
g := f
|
||||||
|
g(new(int)) // ERROR "new\(int\) does not escape"
|
||||||
|
|
||||||
|
h := nopFunc2
|
||||||
|
h(new(int)) // ERROR "new\(int\) does not escape"
|
||||||
|
}
|
||||||
|
|
||||||
|
func nopFunc2(p *int) *int { return p } // ERROR "leaking param: p to result ~r0 level=0"
|
||||||
|
|
|
@ -9,18 +9,18 @@
|
||||||
|
|
||||||
package foo
|
package foo
|
||||||
|
|
||||||
func small(a []int) int { // ERROR "can inline small with cost .* as:.*" "a does not escape"
|
func small(a []int) int { // ERROR "can inline small with cost .* as:.*" "a does not escape" "does not mutate param: a" "does not call param: a"
|
||||||
// Cost 16 body (need cost < 20).
|
// Cost 16 body (need cost < 20).
|
||||||
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
||||||
return a[0] + a[1] + a[2] + a[3]
|
return a[0] + a[1] + a[2] + a[3]
|
||||||
}
|
}
|
||||||
func medium(a []int) int { // ERROR "can inline medium with cost .* as:.*" "a does not escape"
|
func medium(a []int) int { // ERROR "can inline medium with cost .* as:.*" "a does not escape" "does not mutate param: a" "does not call param: a"
|
||||||
// Cost 32 body (need cost > 20 and cost < 80).
|
// Cost 32 body (need cost > 20 and cost < 80).
|
||||||
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
||||||
return a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
|
return a[0] + a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
|
||||||
}
|
}
|
||||||
|
|
||||||
func f(a []int) int { // ERROR "cannot inline f:.*" "a does not escape" "function f considered 'big'"
|
func f(a []int) int { // ERROR "cannot inline f:.*" "a does not escape" "function f considered 'big'" "mutates param: a derefs=0" "does not call param: a"
|
||||||
// Add lots of nodes to f's body. We need >5000.
|
// Add lots of nodes to f's body. We need >5000.
|
||||||
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
// See cmd/compile/internal/gc/inl.go:inlineBigFunction*
|
||||||
a[0] = 0
|
a[0] = 0
|
||||||
|
|
Loading…
Reference in a new issue