Skip to content

Commit 1925839

Browse files
prattmicpull[bot]
authored andcommitted
cmd/compile: synchronize inlinability logic between hairyVisitor and mkinlcall
When computing function cost, hairyVisitor.doNode has two primary cases for determining the cost of a call inside the function: * Normal calls are simply cost 57. * Calls that can be inlined have the cost of the inlined function body, since that body will end up in this function. Determining which "calls can be inlined" is where this breaks down. doNode simply assumes that any function with `fn.Inl != nil` will get inlined. However, this are more complex in mkinlcall, which has a variety of cases where it may not inline. For standard builds, most of these reasons are fairly rare (recursive calls, calls to runtime functions in instrumented builds, etc), so this simplification isn't too build. However, for PGO builds, any function involved in at least one inlinable hot callsite will have `fn.Inl != nil`, even though mkinlcall will only inline at the hot callsites. As a result, cold functions calling hot functions will use the (potentially very large) hot function inline body cost in their call budget. This could make these functions too expensive to inline even though they won't actually inline the hot function. Handle this case plus the other inlinability cases (recursive calls, etc) by consolidating mkinlcall's inlinability logic into canInlineCallExpr, which is shared by doNode. mkinlcall and doNode now have identical logic, except for one case: we check for recursive cycles via inlined functions by looking at the inline tree. Since we haven't actually done any inlining yet when in doNode, we will miss those cases. This CL doesn't change any inlining decisions in a standard build of the compiler. In the new inliner, the inlining decision is also based on the call site, so this synchronization is also helpful. Fixes golang#59484 Change-Id: I6ace66e37d50526535972215497ef75cd71f8b9d Reviewed-on: https://go-review.googlesource.com/c/go/+/483196 Reviewed-by: Than McIntosh <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent 281016d commit 1925839

File tree

1 file changed

+78
-58
lines changed
  • src/cmd/compile/internal/inline

1 file changed

+78
-58
lines changed

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

Lines changed: 78 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ func CanInline(fn *ir.Func, profile *pgo.Profile) {
361361

362362
visitor := hairyVisitor{
363363
curFunc: fn,
364+
isBigFunc: isBigFunc(fn),
364365
budget: budget,
365366
maxBudget: budget,
366367
extraCallCost: cc,
@@ -499,6 +500,7 @@ func canDelayResults(fn *ir.Func) bool {
499500
type hairyVisitor struct {
500501
// This is needed to access the current caller in the doNode function.
501502
curFunc *ir.Func
503+
isBigFunc bool
502504
budget int32
503505
maxBudget int32
504506
reason string
@@ -600,41 +602,29 @@ opSwitch:
600602
break // treat like any other node, that is, cost of 1
601603
}
602604

603-
// Determine if the callee edge is for an inlinable hot callee or not.
604-
if v.profile != nil && v.curFunc != nil {
605-
if fn := inlCallee(v.curFunc, n.Fun, v.profile); fn != nil && typecheck.HaveInlineBody(fn) {
606-
lineOffset := pgo.NodeLineOffset(n, fn)
607-
csi := pgo.CallSiteInfo{LineOffset: lineOffset, Caller: v.curFunc}
608-
if _, o := candHotEdgeMap[csi]; o {
609-
if base.Debug.PGODebug > 0 {
610-
fmt.Printf("hot-callsite identified at line=%v for func=%v\n", ir.Line(n), ir.PkgFuncName(v.curFunc))
611-
}
612-
}
613-
}
614-
}
615-
616605
if ir.IsIntrinsicCall(n) {
617606
// Treat like any other node.
618607
break
619608
}
620609

621-
if fn := inlCallee(v.curFunc, n.Fun, v.profile); fn != nil && typecheck.HaveInlineBody(fn) {
622-
// In the existing inliner, it makes sense to use fn.Inl.Cost
623-
// here due to the fact that an "inline F everywhere if F inlinable"
624-
// strategy is used. With the new inliner, however, it is not
625-
// a given that we'll inline a specific callsite -- it depends
626-
// on what score we assign to the callsite. For now, use the
627-
// computed cost if lower than the call cost, otherwise
628-
// use call cost (we can eventually do away with this when
629-
// we move to the "min-heap of callsites" scheme.
630-
if !goexperiment.NewInliner {
631-
v.budget -= fn.Inl.Cost
610+
if callee := inlCallee(v.curFunc, n.Fun, v.profile); callee != nil && typecheck.HaveInlineBody(callee) {
611+
// Check whether we'd actually inline this call. Set
612+
// log == false since we aren't actually doing inlining
613+
// yet.
614+
if canInlineCallExpr(v.curFunc, n, callee, v.isBigFunc, false) {
615+
// mkinlcall would inline this call [1], so use
616+
// the cost of the inline body as the cost of
617+
// the call, as that is what will actually
618+
// appear in the code.
619+
//
620+
// [1] This is almost a perfect match to the
621+
// mkinlcall logic, except that
622+
// canInlineCallExpr considers inlining cycles
623+
// by looking at what has already been inlined.
624+
// Since we haven't done any inlining yet we
625+
// will miss those.
626+
v.budget -= callee.Inl.Cost
632627
break
633-
} else {
634-
if fn.Inl.Cost < inlineExtraCallCost {
635-
v.budget -= fn.Inl.Cost
636-
break
637-
}
638628
}
639629
}
640630

@@ -1056,54 +1046,59 @@ func inlineCostOK(n *ir.CallExpr, caller, callee *ir.Func, bigCaller bool) (bool
10561046
return true, 0
10571047
}
10581048

1059-
// If n is a OCALLFUNC node, and fn is an ONAME node for a
1060-
// function with an inlinable body, return an OINLCALL node that can replace n.
1061-
// The returned node's Ninit has the parameter assignments, the Nbody is the
1062-
// inlined function body, and (List, Rlist) contain the (input, output)
1063-
// parameters.
1064-
// The result of mkinlcall MUST be assigned back to n, e.g.
1049+
// canInlineCallsite returns true if the call n from caller to callee can be
1050+
// inlined. bigCaller indicates that caller is a big function. log indicates
1051+
// that the 'cannot inline' reason should be logged.
10651052
//
1066-
// n.Left = mkinlcall(n.Left, fn, isddd)
1067-
func mkinlcall(callerfn *ir.Func, n *ir.CallExpr, fn *ir.Func, bigCaller bool, inlCalls *[]*ir.InlinedCallExpr) ir.Node {
1068-
if fn.Inl == nil {
1069-
if logopt.Enabled() {
1053+
// Preconditions: CanInline(callee) has already been called.
1054+
func canInlineCallExpr(callerfn *ir.Func, n *ir.CallExpr, callee *ir.Func, bigCaller bool, log bool) bool {
1055+
if callee.Inl == nil {
1056+
// callee is never inlinable.
1057+
if log && logopt.Enabled() {
10701058
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", ir.FuncName(callerfn),
1071-
fmt.Sprintf("%s cannot be inlined", ir.PkgFuncName(fn)))
1059+
fmt.Sprintf("%s cannot be inlined", ir.PkgFuncName(callee)))
10721060
}
1073-
return n
1061+
return false
10741062
}
10751063

1076-
if ok, maxCost := inlineCostOK(n, callerfn, fn, bigCaller); !ok {
1077-
if logopt.Enabled() {
1064+
if ok, maxCost := inlineCostOK(n, callerfn, callee, bigCaller); !ok {
1065+
// callee cost too high for this call site.
1066+
if log && logopt.Enabled() {
10781067
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", ir.FuncName(callerfn),
1079-
fmt.Sprintf("cost %d of %s exceeds max caller cost %d", fn.Inl.Cost, ir.PkgFuncName(fn), maxCost))
1068+
fmt.Sprintf("cost %d of %s exceeds max caller cost %d", callee.Inl.Cost, ir.PkgFuncName(callee), maxCost))
10801069
}
1081-
return n
1070+
return false
10821071
}
10831072

1084-
if fn == callerfn {
1073+
if callee == callerfn {
10851074
// Can't recursively inline a function into itself.
1086-
if logopt.Enabled() {
1075+
if log && logopt.Enabled() {
10871076
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", fmt.Sprintf("recursive call to %s", ir.FuncName(callerfn)))
10881077
}
1089-
return n
1078+
return false
10901079
}
10911080

1092-
if base.Flag.Cfg.Instrumenting && types.IsNoInstrumentPkg(fn.Sym().Pkg) {
1081+
if base.Flag.Cfg.Instrumenting && types.IsNoInstrumentPkg(callee.Sym().Pkg) {
10931082
// Runtime package must not be instrumented.
10941083
// Instrument skips runtime package. However, some runtime code can be
10951084
// inlined into other packages and instrumented there. To avoid this,
10961085
// we disable inlining of runtime functions when instrumenting.
10971086
// The example that we observed is inlining of LockOSThread,
10981087
// which lead to false race reports on m contents.
1099-
return n
1100-
}
1101-
if base.Flag.Race && types.IsNoRacePkg(fn.Sym().Pkg) {
1102-
return n
1088+
if log && logopt.Enabled() {
1089+
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", ir.FuncName(callerfn),
1090+
fmt.Sprintf("call to runtime function %s in instrumented build", ir.PkgFuncName(callee)))
1091+
}
1092+
return false
11031093
}
11041094

1105-
parent := base.Ctxt.PosTable.Pos(n.Pos()).Base().InliningIndex()
1106-
sym := fn.Linksym()
1095+
if base.Flag.Race && types.IsNoRacePkg(callee.Sym().Pkg) {
1096+
if log && logopt.Enabled() {
1097+
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", ir.FuncName(callerfn),
1098+
fmt.Sprintf(`call to into "no-race" package function %s in race build`, ir.PkgFuncName(callee)))
1099+
}
1100+
return false
1101+
}
11071102

11081103
// Check if we've already inlined this function at this particular
11091104
// call site, in order to stop inlining when we reach the beginning
@@ -1112,17 +1107,42 @@ func mkinlcall(callerfn *ir.Func, n *ir.CallExpr, fn *ir.Func, bigCaller bool, i
11121107
// many functions. Most likely, the inlining will stop before we
11131108
// even hit the beginning of the cycle again, but this catches the
11141109
// unusual case.
1110+
parent := base.Ctxt.PosTable.Pos(n.Pos()).Base().InliningIndex()
1111+
sym := callee.Linksym()
11151112
for inlIndex := parent; inlIndex >= 0; inlIndex = base.Ctxt.InlTree.Parent(inlIndex) {
11161113
if base.Ctxt.InlTree.InlinedFunction(inlIndex) == sym {
1117-
if base.Flag.LowerM > 1 {
1118-
fmt.Printf("%v: cannot inline %v into %v: repeated recursive cycle\n", ir.Line(n), fn, ir.FuncName(callerfn))
1114+
if log {
1115+
if base.Flag.LowerM > 1 {
1116+
fmt.Printf("%v: cannot inline %v into %v: repeated recursive cycle\n", ir.Line(n), callee, ir.FuncName(callerfn))
1117+
}
1118+
if logopt.Enabled() {
1119+
logopt.LogOpt(n.Pos(), "cannotInlineCall", "inline", ir.FuncName(callerfn),
1120+
fmt.Sprintf("repeated recursive cycle to %s", ir.PkgFuncName(callee)))
1121+
}
11191122
}
1120-
return n
1123+
return false
11211124
}
11221125
}
11231126

1127+
return true
1128+
}
1129+
1130+
// If n is a OCALLFUNC node, and fn is an ONAME node for a
1131+
// function with an inlinable body, return an OINLCALL node that can replace n.
1132+
// The returned node's Ninit has the parameter assignments, the Nbody is the
1133+
// inlined function body, and (List, Rlist) contain the (input, output)
1134+
// parameters.
1135+
// The result of mkinlcall MUST be assigned back to n, e.g.
1136+
//
1137+
// n.Left = mkinlcall(n.Left, fn, isddd)
1138+
func mkinlcall(callerfn *ir.Func, n *ir.CallExpr, fn *ir.Func, bigCaller bool, inlCalls *[]*ir.InlinedCallExpr) ir.Node {
1139+
if !canInlineCallExpr(callerfn, n, fn, bigCaller, true) {
1140+
return n
1141+
}
11241142
typecheck.AssertFixedCall(n)
11251143

1144+
parent := base.Ctxt.PosTable.Pos(n.Pos()).Base().InliningIndex()
1145+
sym := fn.Linksym()
11261146
inlIndex := base.Ctxt.InlTree.Add(parent, n.Pos(), sym, ir.FuncName(fn))
11271147

11281148
closureInitLSym := func(n *ir.CallExpr, fn *ir.Func) {

0 commit comments

Comments
 (0)