Skip to content

Commit ed7a833

Browse files
committed
cmd/compile: allow mid-stack inlining when there is a cycle of recursion
We still disallow inlining for an immediately-recursive function, but allow inlining if a function is in a recursion chain. If all functions in the recursion chain are simple, then we could inline forever down the recursion chain (eventually running out of stack on the compiler), so we add a map to keep track of the functions we have already inlined at a call site. We stop inlining when we reach a function that we have already inlined in the recursive chain. Of course, normally the inlining will have stopped earlier, because of the cost function. We could also limit the depth of inlining by a simple count (say, limit max inlining of 10 at any given site). Would that limit other opportunities too much? Added a test in test/inline.go. runtime.BenchmarkStackCopyNoCache() is also already a good test that triggers the check to stop inlining when we reach the start of the recursive chain again. For the bent benchmark suite, the performance improvement was mostly not statistically significant, but the geomean averaged out to: -0.68%. The text size increase was less than .1% for all bent benchmarks. The cmd/go text size increase was 0.02% and the cmd/compile text size increase was .1%. Fixes #29737 Change-Id: I892fa84bb07a947b3125ec8f25ed0e508bf2bdf5 Reviewed-on: https://go-review.googlesource.com/c/go/+/226818 Run-TryBot: Dan Scales <danscales@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Keith Randall <khr@golang.org>
1 parent 339e9c6 commit ed7a833

File tree

4 files changed

+69
-18
lines changed

4 files changed

+69
-18
lines changed

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

+34-17
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,14 @@ func inlcalls(fn *Node) {
496496
if countNodes(fn) >= inlineBigFunctionNodes {
497497
maxCost = inlineBigFunctionMaxCost
498498
}
499-
fn = inlnode(fn, maxCost)
499+
// Map to keep track of functions that have been inlined at a particular
500+
// call site, in order to stop inlining when we reach the beginning of a
501+
// recursion cycle again. We don't inline immediately recursive functions,
502+
// but allow inlining if there is a recursion cycle of many functions.
503+
// Most likely, the inlining will stop before we even hit the beginning of
504+
// the cycle again, but the map catches the unusual case.
505+
inlMap := make(map[*Node]bool)
506+
fn = inlnode(fn, maxCost, inlMap)
500507
if fn != Curfn {
501508
Fatalf("inlnode replaced curfn")
502509
}
@@ -537,10 +544,10 @@ func inlconv2list(n *Node) []*Node {
537544
return s
538545
}
539546

540-
func inlnodelist(l Nodes, maxCost int32) {
547+
func inlnodelist(l Nodes, maxCost int32, inlMap map[*Node]bool) {
541548
s := l.Slice()
542549
for i := range s {
543-
s[i] = inlnode(s[i], maxCost)
550+
s[i] = inlnode(s[i], maxCost, inlMap)
544551
}
545552
}
546553

@@ -557,7 +564,7 @@ func inlnodelist(l Nodes, maxCost int32) {
557564
// shorter and less complicated.
558565
// The result of inlnode MUST be assigned back to n, e.g.
559566
// n.Left = inlnode(n.Left)
560-
func inlnode(n *Node, maxCost int32) *Node {
567+
func inlnode(n *Node, maxCost int32, inlMap map[*Node]bool) *Node {
561568
if n == nil {
562569
return n
563570
}
@@ -585,19 +592,19 @@ func inlnode(n *Node, maxCost int32) *Node {
585592

586593
lno := setlineno(n)
587594

588-
inlnodelist(n.Ninit, maxCost)
595+
inlnodelist(n.Ninit, maxCost, inlMap)
589596
for _, n1 := range n.Ninit.Slice() {
590597
if n1.Op == OINLCALL {
591598
inlconv2stmt(n1)
592599
}
593600
}
594601

595-
n.Left = inlnode(n.Left, maxCost)
602+
n.Left = inlnode(n.Left, maxCost, inlMap)
596603
if n.Left != nil && n.Left.Op == OINLCALL {
597604
n.Left = inlconv2expr(n.Left)
598605
}
599606

600-
n.Right = inlnode(n.Right, maxCost)
607+
n.Right = inlnode(n.Right, maxCost, inlMap)
601608
if n.Right != nil && n.Right.Op == OINLCALL {
602609
if n.Op == OFOR || n.Op == OFORUNTIL {
603610
inlconv2stmt(n.Right)
@@ -612,7 +619,7 @@ func inlnode(n *Node, maxCost int32) *Node {
612619
}
613620
}
614621

615-
inlnodelist(n.List, maxCost)
622+
inlnodelist(n.List, maxCost, inlMap)
616623
if n.Op == OBLOCK {
617624
for _, n2 := range n.List.Slice() {
618625
if n2.Op == OINLCALL {
@@ -628,7 +635,7 @@ func inlnode(n *Node, maxCost int32) *Node {
628635
}
629636
}
630637

631-
inlnodelist(n.Rlist, maxCost)
638+
inlnodelist(n.Rlist, maxCost, inlMap)
632639
s := n.Rlist.Slice()
633640
for i1, n1 := range s {
634641
if n1.Op == OINLCALL {
@@ -640,7 +647,7 @@ func inlnode(n *Node, maxCost int32) *Node {
640647
}
641648
}
642649

643-
inlnodelist(n.Nbody, maxCost)
650+
inlnodelist(n.Nbody, maxCost, inlMap)
644651
for _, n := range n.Nbody.Slice() {
645652
if n.Op == OINLCALL {
646653
inlconv2stmt(n)
@@ -663,12 +670,12 @@ func inlnode(n *Node, maxCost int32) *Node {
663670
fmt.Printf("%v:call to func %+v\n", n.Line(), n.Left)
664671
}
665672
if n.Left.Func != nil && n.Left.Func.Inl != nil && !isIntrinsicCall(n) { // normal case
666-
n = mkinlcall(n, n.Left, maxCost)
673+
n = mkinlcall(n, n.Left, maxCost, inlMap)
667674
} else if n.Left.isMethodExpression() && asNode(n.Left.Sym.Def) != nil {
668-
n = mkinlcall(n, asNode(n.Left.Sym.Def), maxCost)
675+
n = mkinlcall(n, asNode(n.Left.Sym.Def), maxCost, inlMap)
669676
} else if n.Left.Op == OCLOSURE {
670677
if f := inlinableClosure(n.Left); f != nil {
671-
n = mkinlcall(n, f, maxCost)
678+
n = mkinlcall(n, f, maxCost, inlMap)
672679
}
673680
} else if n.Left.Op == ONAME && n.Left.Name != nil && n.Left.Name.Defn != nil {
674681
if d := n.Left.Name.Defn; d.Op == OAS && d.Right.Op == OCLOSURE {
@@ -694,7 +701,7 @@ func inlnode(n *Node, maxCost int32) *Node {
694701
}
695702
break
696703
}
697-
n = mkinlcall(n, f, maxCost)
704+
n = mkinlcall(n, f, maxCost, inlMap)
698705
}
699706
}
700707
}
@@ -713,7 +720,7 @@ func inlnode(n *Node, maxCost int32) *Node {
713720
Fatalf("no function definition for [%p] %+v\n", n.Left.Type, n.Left.Type)
714721
}
715722

716-
n = mkinlcall(n, asNode(n.Left.Type.FuncType().Nname), maxCost)
723+
n = mkinlcall(n, asNode(n.Left.Type.FuncType().Nname), maxCost, inlMap)
717724
}
718725

719726
lineno = lno
@@ -833,7 +840,7 @@ var inlgen int
833840
// parameters.
834841
// The result of mkinlcall MUST be assigned back to n, e.g.
835842
// n.Left = mkinlcall(n.Left, fn, isddd)
836-
func mkinlcall(n, fn *Node, maxCost int32) *Node {
843+
func mkinlcall(n, fn *Node, maxCost int32, inlMap map[*Node]bool) *Node {
837844
if fn.Func.Inl == nil {
838845
// No inlinable body.
839846
return n
@@ -866,6 +873,16 @@ func mkinlcall(n, fn *Node, maxCost int32) *Node {
866873
return n
867874
}
868875

876+
if inlMap[fn] {
877+
if Debug['m'] > 1 {
878+
fmt.Printf("%v: cannot inline %v into %v: repeated recursive cycle\n", n.Line(), fn, Curfn.funcname())
879+
}
880+
return n
881+
}
882+
inlMap[fn] = true
883+
defer func() {
884+
inlMap[fn] = false
885+
}()
869886
if Debug_typecheckinl == 0 {
870887
typecheckinl(fn)
871888
}
@@ -1129,7 +1146,7 @@ func mkinlcall(n, fn *Node, maxCost int32) *Node {
11291146
// instead we emit the things that the body needs
11301147
// and each use must redo the inlining.
11311148
// luckily these are small.
1132-
inlnodelist(call.Nbody, maxCost)
1149+
inlnodelist(call.Nbody, maxCost, inlMap)
11331150
for _, n := range call.Nbody.Slice() {
11341151
if n.Op == OINLCALL {
11351152
inlconv2stmt(n)

src/cmd/compile/internal/gc/main.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -679,8 +679,12 @@ func Main(archInit func(*Arch)) {
679679
if Debug['l'] != 0 {
680680
// Find functions that can be inlined and clone them before walk expands them.
681681
visitBottomUp(xtop, func(list []*Node, recursive bool) {
682+
numfns := numNonClosures(list)
682683
for _, n := range list {
683-
if !recursive {
684+
if !recursive || numfns > 1 {
685+
// We allow inlining if there is no
686+
// recursion, or the recursion cycle is
687+
// across more than one function.
684688
caninl(n)
685689
} else {
686690
if Debug['m'] > 1 {
@@ -824,6 +828,17 @@ func Main(archInit func(*Arch)) {
824828
}
825829
}
826830

831+
// numNonClosures returns the number of functions in list which are not closures.
832+
func numNonClosures(list []*Node) int {
833+
count := 0
834+
for _, n := range list {
835+
if n.Func.Closure == nil {
836+
count++
837+
}
838+
}
839+
return count
840+
}
841+
827842
func writebench(filename string) error {
828843
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
829844
if err != nil {

test/inline.go

+18
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,21 @@ func (T) meth2(int, int) { // not inlineable - has 2 calls.
180180
runtime.GC()
181181
runtime.GC()
182182
}
183+
184+
// Issue #29737 - make sure we can do inlining for a chain of recursive functions
185+
func ee() { // ERROR "can inline ee"
186+
ff(100) // ERROR "inlining call to ff" "inlining call to gg" "inlining call to hh"
187+
}
188+
189+
func ff(x int) { // ERROR "can inline ff"
190+
if x < 0 {
191+
return
192+
}
193+
gg(x - 1)
194+
}
195+
func gg(x int) { // ERROR "can inline gg"
196+
hh(x - 1)
197+
}
198+
func hh(x int) { // ERROR "can inline hh"
199+
ff(x - 1) // ERROR "inlining call to ff" // ERROR "inlining call to gg"
200+
}

test/nowritebarrier.go

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func d2() {
6767
d3()
6868
}
6969

70+
//go:noinline
7071
func d3() {
7172
x.f = y // ERROR "write barrier prohibited by caller"
7273
d4()

0 commit comments

Comments
 (0)