]> Cypherpunks.ru repositories - gostls13.git/commitdiff
cmd/compile: allow more inlining of functions that construct closures
authorThan McIntosh <thanm@google.com>
Tue, 4 Apr 2023 22:31:46 +0000 (18:31 -0400)
committerThan McIntosh <thanm@google.com>
Mon, 17 Apr 2023 14:52:41 +0000 (14:52 +0000)
[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 <thanm@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
src/cmd/compile/internal/inline/inl.go
src/cmd/compile/internal/test/inl_test.go
test/closure3.dir/main.go

index 24812c8c0d67f715de07f36e02bc12a4f63c68fe..81aa200a84607f1255cbedeb3a594736d54718a8 100644 (file)
@@ -506,6 +506,8 @@ func (v *hairyVisitor) tooHairy(fn *ir.Func) bool {
        return false
 }
 
+// doNode visits n and its children, updates the state in v, and returns true if
+// n makes the current function too hairy for inlining.
 func (v *hairyVisitor) doNode(n ir.Node) bool {
        if n == nil {
                return false
@@ -637,13 +639,10 @@ func (v *hairyVisitor) doNode(n ir.Node) bool {
                // TODO(danscales): Maybe make budget proportional to number of closure
                // variables, e.g.:
                //v.budget -= int32(len(n.(*ir.ClosureExpr).Func.ClosureVars) * 3)
+               // TODO(austin): However, if we're able to inline this closure into
+               // v.curFunc, then we actually pay nothing for the closure captures. We
+               // should try to account for that if we're going to account for captures.
                v.budget -= 15
-               // Scan body of closure (which DoChildren doesn't automatically
-               // do) to check for disallowed ops in the body and include the
-               // body in the budget.
-               if doList(n.(*ir.ClosureExpr).Func.Body, v.do) {
-                       return true
-               }
 
        case ir.OGO,
                ir.ODEFER,
index 2a16b21cef76ea77dacd493da4ed6da86ab0cca9..205b746dd8925f31c4ca0a9f56e597949793bcc9 100644 (file)
@@ -180,19 +180,15 @@ func TestIntendedInlining(t *testing.T) {
                "net": {
                        "(*UDPConn).ReadFromUDP",
                },
-               // These testpoints commented out for now, since CL 479095
-               // had to be reverted. We can re-enable this once we roll
-               // forward with a new version of 479095.
-               /*
-                       "sync": {
-                               // Both OnceFunc and its returned closure need to be inlinable so
-                               // that the returned closure can be inlined into the caller of OnceFunc.
-                               "OnceFunc",
-                               "OnceFunc.func2", // The returned closure.
-                               // TODO(austin): It would be good to check OnceValue and OnceValues,
-                               // too, but currently they aren't reported because they have type
-                               // parameters and aren't instantiated in sync.
-                       }, */
+               "sync": {
+                       // Both OnceFunc and its returned closure need to be inlinable so
+                       // that the returned closure can be inlined into the caller of OnceFunc.
+                       "OnceFunc",
+                       "OnceFunc.func2", // The returned closure.
+                       // TODO(austin): It would be good to check OnceValue and OnceValues,
+                       // too, but currently they aren't reported because they have type
+                       // parameters and aren't instantiated in sync.
+               },
                "sync/atomic": {
                        // (*Bool).CompareAndSwap handled below.
                        "(*Bool).Load",
index 4d02a4d10ede896e89c392e543242ded2756c823..04a669206e53382256369961807e678616aaee8e 100644 (file)
@@ -232,15 +232,15 @@ func main() {
 
        {
                c := 3
-               func() { // ERROR "func literal does not escape"
+               func() { // ERROR "can inline main.func26"
                        c = 4
-                       func() { // ERROR "func literal does not escape"
+                       func() {
                                if c != 4 {
                                        ppanic("c != 4")
                                }
                                recover() // prevent inlining
                        }()
-               }()
+               }() // ERROR "inlining call to main.func26" "func literal does not escape"
                if c != 4 {
                        ppanic("c != 4")
                }
@@ -248,33 +248,37 @@ func main() {
 
        {
                a := 2
-               if r := func(x int) int { // ERROR "func literal does not escape"
+               // This has an unfortunate exponential growth, where as we visit each
+               // function, we inline the inner closure, and that constructs a new
+               // function for any closures inside the inner function, and then we
+               // revisit those. E.g., func34 and func36 are constructed by the inliner.
+               if r := func(x int) int { // ERROR "can inline main.func27"
                        b := 3
-                       return func(y int) int { // ERROR "can inline main.func27.1"
+                       return func(y int) int { // ERROR "can inline main.func27.1" "can inline main.func34"
                                c := 5
-                               return func(z int) int { // ERROR "can inline main.func27.1.1" "can inline main.func27.(func)?2"
+                               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"
                                        return a*x + b*y + c*z
                                }(10) // ERROR "inlining call to main.func27.1.1"
                        }(100) // ERROR "inlining call to main.func27.1" "inlining call to main.func27.(func)?2"
-               }(1000); r != 2350 {
+               }(1000); r != 2350 { // ERROR "inlining call to main.func27" "inlining call to main.func34" "inlining call to main.func36"
                        ppanic("r != 2350")
                }
        }
 
        {
                a := 2
-               if r := func(x int) int { // ERROR "func literal does not escape"
+               if r := func(x int) int { // ERROR "can inline main.func28"
                        b := 3
-                       return func(y int) int { // ERROR "can inline main.func28.1"
+                       return func(y int) int { // ERROR "can inline main.func28.1" "can inline main.func35"
                                c := 5
-                               func(z int) { // ERROR "can inline main.func28.1.1" "can inline main.func28.(func)?2"
+                               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"
                                        a = a * x
                                        b = b * y
                                        c = c * z
                                }(10) // ERROR "inlining call to main.func28.1.1"
                                return a + c
                        }(100) + b // ERROR "inlining call to main.func28.1" "inlining call to main.func28.(func)?2"
-               }(1000); r != 2350 {
+               }(1000); r != 2350 { // ERROR "inlining call to main.func28" "inlining call to main.func35" "inlining call to main.func37"
                        ppanic("r != 2350")
                }
                if a != 2000 {