Skip to content

Commit f8162a0

Browse files
committed
cmd/compile: allow more inlining of functions that construct closures
[This is a roll-forward of CL 479095, which was reverted due to a bad interaction between inlining and escape analysis, then later fixed fist with an attempt in CL 482355, then again in 484859 .] Currently, when the inliner is determining if a function is inlineable, it descends into the bodies of closures constructed by that function. This has several unfortunate consequences: - If the closure contains a disallowed operation (e.g., a defer), then the outer function can't be inlined. It makes sense that the *closure* can't be inlined in this case, but it doesn't make sense to punish the function that constructs the closure. - The hairiness of the closure counts against the inlining budget of the outer function. Since we currently copy the closure body when inlining the outer function, this makes sense from the perspective of export data size and binary size, but ultimately doesn't make much sense from the perspective of what should be inlineable. - Since the inliner walks into every closure created by an outer function in addition to starting a walk at every closure, this adds an n^2 factor to inlinability analysis. This CL simply drops this behavior. In std, this makes 57 more functions inlinable, and disallows inlining for 10 (due to the basic instability of our bottom-up inlining approach), for an net increase of 47 inlinable functions (+0.6%). This will help significantly with the performance of the functions to be added for #56102, which have a somewhat complicated nesting of closures with a performance-critical fast path. The downside of this seems to be a potential increase in export data and text size, but the practical impact of this seems to be negligible: │ before │ after │ │ bytes │ bytes vs base │ Go/binary 15.12Mi ± 0% 15.14Mi ± 0% +0.16% (n=1) Go/text 5.220Mi ± 0% 5.237Mi ± 0% +0.32% (n=1) Compile/binary 22.92Mi ± 0% 22.94Mi ± 0% +0.07% (n=1) Compile/text 8.428Mi ± 0% 8.435Mi ± 0% +0.08% (n=1) Updates #56102. Change-Id: I6e938d596992ffb473cf51e7e598f372ce08deb0 Reviewed-on: https://go-review.googlesource.com/c/go/+/484860 Run-TryBot: Than McIntosh <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]> Reviewed-by: Cuong Manh Le <[email protected]>
1 parent d240226 commit f8162a0

File tree

3 files changed

+29
-30
lines changed

3 files changed

+29
-30
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ func (v *hairyVisitor) tooHairy(fn *ir.Func) bool {
506506
return false
507507
}
508508

509+
// doNode visits n and its children, updates the state in v, and returns true if
510+
// n makes the current function too hairy for inlining.
509511
func (v *hairyVisitor) doNode(n ir.Node) bool {
510512
if n == nil {
511513
return false
@@ -637,13 +639,10 @@ func (v *hairyVisitor) doNode(n ir.Node) bool {
637639
// TODO(danscales): Maybe make budget proportional to number of closure
638640
// variables, e.g.:
639641
//v.budget -= int32(len(n.(*ir.ClosureExpr).Func.ClosureVars) * 3)
642+
// TODO(austin): However, if we're able to inline this closure into
643+
// v.curFunc, then we actually pay nothing for the closure captures. We
644+
// should try to account for that if we're going to account for captures.
640645
v.budget -= 15
641-
// Scan body of closure (which DoChildren doesn't automatically
642-
// do) to check for disallowed ops in the body and include the
643-
// body in the budget.
644-
if doList(n.(*ir.ClosureExpr).Func.Body, v.do) {
645-
return true
646-
}
647646

648647
case ir.OGO,
649648
ir.ODEFER,

src/cmd/compile/internal/test/inl_test.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,19 +180,15 @@ func TestIntendedInlining(t *testing.T) {
180180
"net": {
181181
"(*UDPConn).ReadFromUDP",
182182
},
183-
// These testpoints commented out for now, since CL 479095
184-
// had to be reverted. We can re-enable this once we roll
185-
// forward with a new version of 479095.
186-
/*
187-
"sync": {
188-
// Both OnceFunc and its returned closure need to be inlinable so
189-
// that the returned closure can be inlined into the caller of OnceFunc.
190-
"OnceFunc",
191-
"OnceFunc.func2", // The returned closure.
192-
// TODO(austin): It would be good to check OnceValue and OnceValues,
193-
// too, but currently they aren't reported because they have type
194-
// parameters and aren't instantiated in sync.
195-
}, */
183+
"sync": {
184+
// Both OnceFunc and its returned closure need to be inlinable so
185+
// that the returned closure can be inlined into the caller of OnceFunc.
186+
"OnceFunc",
187+
"OnceFunc.func2", // The returned closure.
188+
// TODO(austin): It would be good to check OnceValue and OnceValues,
189+
// too, but currently they aren't reported because they have type
190+
// parameters and aren't instantiated in sync.
191+
},
196192
"sync/atomic": {
197193
// (*Bool).CompareAndSwap handled below.
198194
"(*Bool).Load",

test/closure3.dir/main.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,49 +232,53 @@ func main() {
232232

233233
{
234234
c := 3
235-
func() { // ERROR "func literal does not escape"
235+
func() { // ERROR "can inline main.func26"
236236
c = 4
237-
func() { // ERROR "func literal does not escape"
237+
func() {
238238
if c != 4 {
239239
ppanic("c != 4")
240240
}
241241
recover() // prevent inlining
242242
}()
243-
}()
243+
}() // ERROR "inlining call to main.func26" "func literal does not escape"
244244
if c != 4 {
245245
ppanic("c != 4")
246246
}
247247
}
248248

249249
{
250250
a := 2
251-
if r := func(x int) int { // ERROR "func literal does not escape"
251+
// This has an unfortunate exponential growth, where as we visit each
252+
// function, we inline the inner closure, and that constructs a new
253+
// function for any closures inside the inner function, and then we
254+
// revisit those. E.g., func34 and func36 are constructed by the inliner.
255+
if r := func(x int) int { // ERROR "can inline main.func27"
252256
b := 3
253-
return func(y int) int { // ERROR "can inline main.func27.1"
257+
return func(y int) int { // ERROR "can inline main.func27.1" "can inline main.func34"
254258
c := 5
255-
return func(z int) int { // ERROR "can inline main.func27.1.1" "can inline main.func27.(func)?2"
259+
return func(z int) int { // ERROR "can inline main.func27.1.1" "can inline main.func27.(func)?2" "can inline main.func34.1" "can inline main.func36"
256260
return a*x + b*y + c*z
257261
}(10) // ERROR "inlining call to main.func27.1.1"
258262
}(100) // ERROR "inlining call to main.func27.1" "inlining call to main.func27.(func)?2"
259-
}(1000); r != 2350 {
263+
}(1000); r != 2350 { // ERROR "inlining call to main.func27" "inlining call to main.func34" "inlining call to main.func36"
260264
ppanic("r != 2350")
261265
}
262266
}
263267

264268
{
265269
a := 2
266-
if r := func(x int) int { // ERROR "func literal does not escape"
270+
if r := func(x int) int { // ERROR "can inline main.func28"
267271
b := 3
268-
return func(y int) int { // ERROR "can inline main.func28.1"
272+
return func(y int) int { // ERROR "can inline main.func28.1" "can inline main.func35"
269273
c := 5
270-
func(z int) { // ERROR "can inline main.func28.1.1" "can inline main.func28.(func)?2"
274+
func(z int) { // ERROR "can inline main.func28.1.1" "can inline main.func28.(func)?2" "can inline main.func35.1" "can inline main.func37"
271275
a = a * x
272276
b = b * y
273277
c = c * z
274278
}(10) // ERROR "inlining call to main.func28.1.1"
275279
return a + c
276280
}(100) + b // ERROR "inlining call to main.func28.1" "inlining call to main.func28.(func)?2"
277-
}(1000); r != 2350 {
281+
}(1000); r != 2350 { // ERROR "inlining call to main.func28" "inlining call to main.func35" "inlining call to main.func37"
278282
ppanic("r != 2350")
279283
}
280284
if a != 2000 {

0 commit comments

Comments
 (0)