Skip to content

Commit d524c95

Browse files
dr2chasegopherbot
authored andcommitted
cmd/compile: use very high budget for once-called closures
This should make it much more likely that rangefunc iterators become "plain inline code". Change-Id: I8026603afdc5249f60cc663c4bc15cb1d26d1c83 Reviewed-on: https://go-review.googlesource.com/c/go/+/630696 Reviewed-by: Keith Randall <[email protected]> Auto-Submit: David Chase <[email protected]> Reviewed-by: Keith Randall <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 8b97607 commit d524c95

File tree

3 files changed

+167
-66
lines changed

3 files changed

+167
-66
lines changed

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

+61-26
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ const (
5353
inlineExtraPanicCost = 1 // do not penalize inlining panics.
5454
inlineExtraThrowCost = inlineMaxBudget // with current (2018-05/1.11) code, inlining runtime.throw does not help.
5555

56-
inlineBigFunctionNodes = 5000 // Functions with this many nodes are considered "big".
57-
inlineBigFunctionMaxCost = 20 // Max cost of inlinee when inlining into a "big" function.
56+
inlineBigFunctionNodes = 5000 // Functions with this many nodes are considered "big".
57+
inlineBigFunctionMaxCost = 20 // Max cost of inlinee when inlining into a "big" function.
58+
inlineClosureCalledOnceCost = 10 * inlineMaxBudget // if a closure is just called once, inline it.
5859
)
5960

6061
var (
@@ -207,7 +208,8 @@ func inlineBudget(fn *ir.Func, profile *pgoir.Profile, relaxed bool, verbose boo
207208
budget += inlheur.BudgetExpansion(inlineMaxBudget)
208209
}
209210
if fn.ClosureParent != nil {
210-
budget *= 2
211+
// be very liberal here, if the closure is only called once, the budget is large
212+
budget = max(budget, inlineClosureCalledOnceCost)
211213
}
212214
return budget
213215
}
@@ -561,11 +563,11 @@ opSwitch:
561563
break
562564
}
563565

564-
if callee := inlCallee(v.curFunc, n.Fun, v.profile); callee != nil && typecheck.HaveInlineBody(callee) {
566+
if callee := inlCallee(v.curFunc, n.Fun, v.profile, false); callee != nil && typecheck.HaveInlineBody(callee) {
565567
// Check whether we'd actually inline this call. Set
566568
// log == false since we aren't actually doing inlining
567569
// yet.
568-
if ok, _, _ := canInlineCallExpr(v.curFunc, n, callee, v.isBigFunc, false); ok {
570+
if ok, _, _ := canInlineCallExpr(v.curFunc, n, callee, v.isBigFunc, false, false); ok {
569571
// mkinlcall would inline this call [1], so use
570572
// the cost of the inline body as the cost of
571573
// the call, as that is what will actually
@@ -577,6 +579,9 @@ opSwitch:
577579
// by looking at what has already been inlined.
578580
// Since we haven't done any inlining yet we
579581
// will miss those.
582+
//
583+
// TODO: in the case of a single-call closure, the inlining budget here is potentially much, much larger.
584+
//
580585
v.budget -= callee.Inl.Cost
581586
break
582587
}
@@ -774,17 +779,18 @@ func IsBigFunc(fn *ir.Func) bool {
774779
})
775780
}
776781

777-
// TryInlineCall returns an inlined call expression for call, or nil
778-
// if inlining is not possible.
779-
func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile *pgoir.Profile) *ir.InlinedCallExpr {
782+
// inlineCallCheck returns whether a call will never be inlineable
783+
// for basic reasons, and whether the call is an intrinisic call.
784+
// The intrinsic result singles out intrinsic calls for debug logging.
785+
func inlineCallCheck(callerfn *ir.Func, call *ir.CallExpr) (bool, bool) {
780786
if base.Flag.LowerL == 0 {
781-
return nil
787+
return false, false
782788
}
783789
if call.Op() != ir.OCALLFUNC {
784-
return nil
790+
return false, false
785791
}
786792
if call.GoDefer || call.NoInline {
787-
return nil
793+
return false, false
788794
}
789795

790796
// Prevent inlining some reflect.Value methods when using checkptr,
@@ -793,26 +799,49 @@ func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile
793799
if method := ir.MethodExprName(call.Fun); method != nil {
794800
switch types.ReflectSymName(method.Sym()) {
795801
case "Value.UnsafeAddr", "Value.Pointer":
796-
return nil
802+
return false, false
797803
}
798804
}
799805
}
806+
if ir.IsIntrinsicCall(call) {
807+
return false, true
808+
}
809+
return true, false
810+
}
811+
812+
// InlineCallTarget returns the resolved-for-inlining target of a call.
813+
// It does not necessarily guarantee that the target can be inlined, though
814+
// obvious exclusions are applied.
815+
func InlineCallTarget(callerfn *ir.Func, call *ir.CallExpr, profile *pgoir.Profile) *ir.Func {
816+
if mightInline, _ := inlineCallCheck(callerfn, call); !mightInline {
817+
return nil
818+
}
819+
return inlCallee(callerfn, call.Fun, profile, true)
820+
}
821+
822+
// TryInlineCall returns an inlined call expression for call, or nil
823+
// if inlining is not possible.
824+
func TryInlineCall(callerfn *ir.Func, call *ir.CallExpr, bigCaller bool, profile *pgoir.Profile, closureCalledOnce bool) *ir.InlinedCallExpr {
825+
mightInline, isIntrinsic := inlineCallCheck(callerfn, call)
800826

801-
if base.Flag.LowerM > 3 {
827+
// Preserve old logging behavior
828+
if (mightInline || isIntrinsic) && base.Flag.LowerM > 3 {
802829
fmt.Printf("%v:call to func %+v\n", ir.Line(call), call.Fun)
803830
}
804-
if ir.IsIntrinsicCall(call) {
831+
if !mightInline {
805832
return nil
806833
}
807-
if fn := inlCallee(callerfn, call.Fun, profile); fn != nil && typecheck.HaveInlineBody(fn) {
808-
return mkinlcall(callerfn, call, fn, bigCaller)
834+
835+
if fn := inlCallee(callerfn, call.Fun, profile, false); fn != nil && typecheck.HaveInlineBody(fn) {
836+
return mkinlcall(callerfn, call, fn, bigCaller, closureCalledOnce)
809837
}
810838
return nil
811839
}
812840

813841
// inlCallee takes a function-typed expression and returns the underlying function ONAME
814842
// that it refers to if statically known. Otherwise, it returns nil.
815-
func inlCallee(caller *ir.Func, fn ir.Node, profile *pgoir.Profile) (res *ir.Func) {
843+
// resolveOnly skips cost-based inlineability checks for closures; the result may not actually be inlineable.
844+
func inlCallee(caller *ir.Func, fn ir.Node, profile *pgoir.Profile, resolveOnly bool) (res *ir.Func) {
816845
fn = ir.StaticValue(fn)
817846
switch fn.Op() {
818847
case ir.OMETHEXPR:
@@ -836,7 +865,9 @@ func inlCallee(caller *ir.Func, fn ir.Node, profile *pgoir.Profile) (res *ir.Fun
836865
if len(c.ClosureVars) != 0 && c.ClosureVars[0].Outer.Curfn != caller {
837866
return nil // inliner doesn't support inlining across closure frames
838867
}
839-
CanInline(c, profile)
868+
if !resolveOnly {
869+
CanInline(c, profile)
870+
}
840871
return c
841872
}
842873
return nil
@@ -862,18 +893,22 @@ var InlineCall = func(callerfn *ir.Func, call *ir.CallExpr, fn *ir.Func, inlInde
862893
// - the "max cost" limit used to make the decision (which may differ depending on func size)
863894
// - the score assigned to this specific callsite
864895
// - whether the inlined function is "hot" according to PGO.
865-
func inlineCostOK(n *ir.CallExpr, caller, callee *ir.Func, bigCaller bool) (bool, int32, int32, bool) {
896+
func inlineCostOK(n *ir.CallExpr, caller, callee *ir.Func, bigCaller, closureCalledOnce bool) (bool, int32, int32, bool) {
866897
maxCost := int32(inlineMaxBudget)
867-
if callee.ClosureParent != nil {
868-
maxCost *= 2 // favor inlining closures
869-
}
870898

871899
if bigCaller {
872900
// We use this to restrict inlining into very big functions.
873901
// See issue 26546 and 17566.
874902
maxCost = inlineBigFunctionMaxCost
875903
}
876904

905+
if callee.ClosureParent != nil {
906+
maxCost *= 2 // favor inlining closures
907+
if closureCalledOnce { // really favor inlining the one call to this closure
908+
maxCost = max(maxCost, inlineClosureCalledOnceCost)
909+
}
910+
}
911+
877912
metric := callee.Inl.Cost
878913
if inlheur.Enabled() {
879914
score, ok := inlheur.GetCallSiteScore(caller, n)
@@ -931,7 +966,7 @@ func inlineCostOK(n *ir.CallExpr, caller, callee *ir.Func, bigCaller bool) (bool
931966
// indicates that the 'cannot inline' reason should be logged.
932967
//
933968
// Preconditions: CanInline(callee) has already been called.
934-
func canInlineCallExpr(callerfn *ir.Func, n *ir.CallExpr, callee *ir.Func, bigCaller bool, log bool) (bool, int32, bool) {
969+
func canInlineCallExpr(callerfn *ir.Func, n *ir.CallExpr, callee *ir.Func, bigCaller, closureCalledOnce bool, log bool) (bool, int32, bool) {
935970
if callee.Inl == nil {
936971
// callee is never inlinable.
937972
if log && logopt.Enabled() {
@@ -941,7 +976,7 @@ func canInlineCallExpr(callerfn *ir.Func, n *ir.CallExpr, callee *ir.Func, bigCa
941976
return false, 0, false
942977
}
943978

944-
ok, maxCost, callSiteScore, hot := inlineCostOK(n, callerfn, callee, bigCaller)
979+
ok, maxCost, callSiteScore, hot := inlineCostOK(n, callerfn, callee, bigCaller, closureCalledOnce)
945980
if !ok {
946981
// callee cost too high for this call site.
947982
if log && logopt.Enabled() {
@@ -1024,8 +1059,8 @@ func canInlineCallExpr(callerfn *ir.Func, n *ir.CallExpr, callee *ir.Func, bigCa
10241059
// The result of mkinlcall MUST be assigned back to n, e.g.
10251060
//
10261061
// n.Left = mkinlcall(n.Left, fn, isddd)
1027-
func mkinlcall(callerfn *ir.Func, n *ir.CallExpr, fn *ir.Func, bigCaller bool) *ir.InlinedCallExpr {
1028-
ok, score, hot := canInlineCallExpr(callerfn, n, fn, bigCaller, true)
1062+
func mkinlcall(callerfn *ir.Func, n *ir.CallExpr, fn *ir.Func, bigCaller, closureCalledOnce bool) *ir.InlinedCallExpr {
1063+
ok, score, hot := canInlineCallExpr(callerfn, n, fn, bigCaller, closureCalledOnce, true)
10291064
if !ok {
10301065
return nil
10311066
}

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

+81-15
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ func DevirtualizeAndInlinePackage(pkg *ir.Package, profile *pgoir.Profile) {
4343
inline.CanInlineFuncs(pkg.Funcs, inlProfile)
4444

4545
inlState := make(map[*ir.Func]*inlClosureState)
46+
calleeUseCounts := make(map[*ir.Func]int)
4647

48+
// Pre-process all the functions, adding parentheses around call sites and starting their "inl state".
4749
for _, fn := range typecheck.Target.Funcs {
48-
// Pre-process all the functions, adding parentheses around call sites.
4950
bigCaller := base.Flag.LowerL != 0 && inline.IsBigFunc(fn)
5051
if bigCaller && base.Flag.LowerM > 1 {
5152
fmt.Printf("%v: function %v considered 'big'; reducing max cost of inlinees\n", ir.Line(fn), fn)
5253
}
5354

54-
s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool)}
55+
s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool), useCounts: calleeUseCounts}
5556
s.parenthesize()
5657
inlState[fn] = s
58+
59+
// Do a first pass at counting call sites.
60+
for i := range s.parens {
61+
s.resolve(i)
62+
}
5763
}
5864

5965
ir.VisitFuncsBottomUp(typecheck.Target.Funcs, func(list []*ir.Func, recursive bool) {
@@ -85,15 +91,34 @@ func DevirtualizeAndInlinePackage(pkg *ir.Package, profile *pgoir.Profile) {
8591
s := inlState[fn]
8692

8793
ir.WithFunc(fn, func() {
88-
for i := 0; i < len(s.parens); i++ { // can't use "range parens" here
89-
paren := s.parens[i]
90-
if new := s.edit(paren.X); new != nil {
91-
// Update AST and recursively mark nodes.
92-
paren.X = new
93-
ir.EditChildren(new, s.mark) // mark may append to parens
94-
done = false
94+
l1 := len(s.parens)
95+
l0 := 0
96+
97+
// Batch iterations so that newly discovered call sites are
98+
// resolved in a batch before inlining attempts.
99+
// Do this to avoid discovering new closure calls 1 at a time
100+
// which might cause first call to be seen as a single (high-budget)
101+
// call before the second is observed.
102+
for {
103+
for i := l0; i < l1; i++ { // can't use "range parens" here
104+
paren := s.parens[i]
105+
if new := s.edit(i); new != nil {
106+
// Update AST and recursively mark nodes.
107+
paren.X = new
108+
ir.EditChildren(new, s.mark) // mark may append to parens
109+
done = false
110+
}
111+
}
112+
l0, l1 = l1, len(s.parens)
113+
if l0 == l1 {
114+
break
95115
}
116+
for i := l0; i < l1; i++ {
117+
s.resolve(i)
118+
}
119+
96120
}
121+
97122
}) // WithFunc
98123

99124
}
@@ -138,29 +163,70 @@ func DevirtualizeAndInlineFunc(fn *ir.Func, profile *pgoir.Profile) {
138163
fmt.Printf("%v: function %v considered 'big'; reducing max cost of inlinees\n", ir.Line(fn), fn)
139164
}
140165

141-
s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool)}
166+
s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool), useCounts: make(map[*ir.Func]int)}
142167
s.parenthesize()
143168
s.fixpoint()
144169
s.unparenthesize()
145170
})
146171
}
147172

173+
type callSite struct {
174+
fn *ir.Func
175+
whichParen int
176+
}
177+
148178
type inlClosureState struct {
149179
fn *ir.Func
150180
profile *pgoir.Profile
151181
callSites map[*ir.ParenExpr]bool // callSites[p] == "p appears in parens" (do not append again)
182+
resolved []*ir.Func // for each call in parens, the resolved target of the call
183+
useCounts map[*ir.Func]int // shared among all InlClosureStates
152184
parens []*ir.ParenExpr
153185
bigCaller bool
154186
}
155187

156-
func (s *inlClosureState) edit(n ir.Node) ir.Node {
188+
// resolve attempts to resolve a call to a potentially inlineable callee
189+
// and updates use counts on the callees. Returns the call site count
190+
// for that callee.
191+
func (s *inlClosureState) resolve(i int) (*ir.Func, int) {
192+
p := s.parens[i]
193+
if i < len(s.resolved) {
194+
if callee := s.resolved[i]; callee != nil {
195+
return callee, s.useCounts[callee]
196+
}
197+
}
198+
n := p.X
157199
call, ok := n.(*ir.CallExpr)
158200
if !ok { // previously inlined
159-
return nil
201+
return nil, -1
160202
}
161-
162203
devirtualize.StaticCall(call)
163-
if inlCall := inline.TryInlineCall(s.fn, call, s.bigCaller, s.profile); inlCall != nil {
204+
if callee := inline.InlineCallTarget(s.fn, call, s.profile); callee != nil {
205+
for len(s.resolved) <= i {
206+
s.resolved = append(s.resolved, nil)
207+
}
208+
s.resolved[i] = callee
209+
c := s.useCounts[callee] + 1
210+
s.useCounts[callee] = c
211+
return callee, c
212+
}
213+
return nil, 0
214+
}
215+
216+
func (s *inlClosureState) edit(i int) ir.Node {
217+
n := s.parens[i].X
218+
call, ok := n.(*ir.CallExpr)
219+
if !ok {
220+
return nil
221+
}
222+
// This is redundant with earlier calls to
223+
// resolve, but because things can change it
224+
// must be re-checked.
225+
callee, count := s.resolve(i)
226+
if count <= 0 {
227+
return nil
228+
}
229+
if inlCall := inline.TryInlineCall(s.fn, call, s.bigCaller, s.profile, count == 1 && callee.ClosureParent != nil); inlCall != nil {
164230
return inlCall
165231
}
166232
return nil
@@ -278,7 +344,7 @@ func (s *inlClosureState) fixpoint() bool {
278344
done = true
279345
for i := 0; i < len(s.parens); i++ { // can't use "range parens" here
280346
paren := s.parens[i]
281-
if new := s.edit(paren.X); new != nil {
347+
if new := s.edit(i); new != nil {
282348
// Update AST and recursively mark nodes.
283349
paren.X = new
284350
ir.EditChildren(new, s.mark) // mark may append to parens

0 commit comments

Comments
 (0)