Skip to content

Commit 4f70a2a

Browse files
huguesbmvdan
authored andcommitted
cmd/compile: inline calls to local closures
Calls to a closure held in a local, non-escaping, variable can be inlined, provided the closure body can be inlined and the variable is never written to. The current implementation has the following limitations: - closures with captured variables are not inlined because doing so naively triggers invariant violation in the SSA phase - re-assignment check is currently approximated by checking the Addrtaken property of the variable which should be safe but may miss optimization opportunities if the address is not used for a write before the invocation Updates #15561 Change-Id: I508cad5d28f027bd7e933b1f793c14dcfef8b5a1 Reviewed-on: https://go-review.googlesource.com/65071 Run-TryBot: Daniel Martí <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Hugues Bruant <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 44d9e96 commit 4f70a2a

File tree

5 files changed

+211
-27
lines changed

5 files changed

+211
-27
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ func (f *bitset8) set(mask uint8, b bool) {
1414
}
1515
}
1616

17+
type bitset16 uint16
18+
19+
func (f *bitset16) set(mask uint16, b bool) {
20+
if b {
21+
*(*uint16)(f) |= mask
22+
} else {
23+
*(*uint16)(f) &^= mask
24+
}
25+
}
26+
1727
type bitset32 uint32
1828

1929
func (f *bitset32) set(mask uint32, b bool) {

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

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ func caninl(fn *Node) {
149149
return
150150
}
151151

152+
n := fn.Func.Nname
153+
if n.Func.InlinabilityChecked() {
154+
return
155+
}
156+
defer n.Func.SetInlinabilityChecked(true)
157+
152158
const maxBudget = 80
153159
visitor := hairyVisitor{budget: maxBudget}
154160
if visitor.visitList(fn.Nbody) {
@@ -163,8 +169,6 @@ func caninl(fn *Node) {
163169
savefn := Curfn
164170
Curfn = fn
165171

166-
n := fn.Func.Nname
167-
168172
n.Func.Inl.Set(fn.Nbody.Slice())
169173
fn.Nbody.Set(inlcopylist(n.Func.Inl.Slice()))
170174
inldcl := inlcopylist(n.Name.Defn.Func.Dcl)
@@ -522,6 +526,37 @@ func inlnode(n *Node) *Node {
522526
n = mkinlcall(n, n.Left, n.Isddd())
523527
} else if n.isMethodCalledAsFunction() && asNode(n.Left.Sym.Def) != nil {
524528
n = mkinlcall(n, asNode(n.Left.Sym.Def), n.Isddd())
529+
} else if n.Left.Op == OCLOSURE {
530+
if f := inlinableClosure(n.Left); f != nil {
531+
n = mkinlcall(n, f, n.Isddd())
532+
}
533+
} else if n.Left.Op == ONAME && n.Left.Name != nil && n.Left.Name.Defn != nil {
534+
if d := n.Left.Name.Defn; d.Op == OAS && d.Right.Op == OCLOSURE {
535+
if f := inlinableClosure(d.Right); f != nil {
536+
// NB: this check is necessary to prevent indirect re-assignment of the variable
537+
// having the address taken after the invocation or only used for reads is actually fine
538+
// but we have no easy way to distinguish the safe cases
539+
if d.Left.Addrtaken() {
540+
if Debug['m'] > 1 {
541+
fmt.Printf("%v: cannot inline escaping closure variable %v\n", n.Line(), n.Left)
542+
}
543+
break
544+
}
545+
546+
// ensure the variable is never re-assigned
547+
if unsafe, a := reassigned(n.Left); unsafe {
548+
if Debug['m'] > 1 {
549+
if a != nil {
550+
fmt.Printf("%v: cannot inline re-assigned closure variable at %v: %v\n", n.Line(), a.Line(), a)
551+
} else {
552+
fmt.Printf("%v: cannot inline global closure variable %v\n", n.Line(), n.Left)
553+
}
554+
}
555+
break
556+
}
557+
n = mkinlcall(n, f, n.Isddd())
558+
}
559+
}
525560
}
526561

527562
case OCALLMETH:
@@ -545,6 +580,95 @@ func inlnode(n *Node) *Node {
545580
return n
546581
}
547582

583+
func inlinableClosure(n *Node) *Node {
584+
c := n.Func.Closure
585+
caninl(c)
586+
f := c.Func.Nname
587+
if f != nil && f.Func.Inl.Len() != 0 {
588+
if n.Func.Cvars.Len() != 0 {
589+
// FIXME: support closure with captured variables
590+
// they currently result in invariant violation in the SSA phase
591+
if Debug['m'] > 1 {
592+
fmt.Printf("%v: cannot inline closure w/ captured vars %v\n", n.Line(), n.Left)
593+
}
594+
return nil
595+
}
596+
return f
597+
}
598+
return nil
599+
}
600+
601+
// reassigned takes an ONAME node, walks the function in which it is defined, and returns a boolean
602+
// indicating whether the name has any assignments other than its declaration.
603+
// The second return value is the first such assignment encountered in the walk, if any. It is mostly
604+
// useful for -m output documenting the reason for inhibited optimizations.
605+
// NB: global variables are always considered to be re-assigned.
606+
// TODO: handle initial declaration not including an assignment and followed by a single assignment?
607+
func reassigned(n *Node) (bool, *Node) {
608+
if n.Op != ONAME {
609+
Fatalf("reassigned %v", n)
610+
}
611+
// no way to reliably check for no-reassignment of globals, assume it can be
612+
if n.Name.Curfn == nil {
613+
return true, nil
614+
}
615+
v := reassignVisitor{name: n}
616+
a := v.visitList(n.Name.Curfn.Nbody)
617+
return a != nil, a
618+
}
619+
620+
type reassignVisitor struct {
621+
name *Node
622+
}
623+
624+
func (v *reassignVisitor) visit(n *Node) *Node {
625+
if n == nil {
626+
return nil
627+
}
628+
switch n.Op {
629+
case OAS:
630+
if n.Left == v.name && n != v.name.Name.Defn {
631+
return n
632+
}
633+
return nil
634+
case OAS2, OAS2FUNC:
635+
for _, p := range n.List.Slice() {
636+
if p == v.name && n != v.name.Name.Defn {
637+
return n
638+
}
639+
}
640+
return nil
641+
}
642+
if a := v.visit(n.Left); a != nil {
643+
return a
644+
}
645+
if a := v.visit(n.Right); a != nil {
646+
return a
647+
}
648+
if a := v.visitList(n.List); a != nil {
649+
return a
650+
}
651+
if a := v.visitList(n.Rlist); a != nil {
652+
return a
653+
}
654+
if a := v.visitList(n.Ninit); a != nil {
655+
return a
656+
}
657+
if a := v.visitList(n.Nbody); a != nil {
658+
return a
659+
}
660+
return nil
661+
}
662+
663+
func (v *reassignVisitor) visitList(l Nodes) *Node {
664+
for _, n := range l.Slice() {
665+
if a := v.visit(n); a != nil {
666+
return a
667+
}
668+
}
669+
return nil
670+
}
671+
548672
// The result of mkinlcall MUST be assigned back to n, e.g.
549673
// n.Left = mkinlcall(n.Left, fn, isddd)
550674
func mkinlcall(n *Node, fn *Node, isddd bool) *Node {

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

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ type Func struct {
384384

385385
Pragma syntax.Pragma // go:xxx function annotations
386386

387-
flags bitset8
387+
flags bitset16
388388
}
389389

390390
// A Mark represents a scope boundary.
@@ -406,28 +406,31 @@ const (
406406
funcNeedctxt // function uses context register (has closure variables)
407407
funcReflectMethod // function calls reflect.Type.Method or MethodByName
408408
funcIsHiddenClosure
409-
funcNoFramePointer // Must not use a frame pointer for this function
410-
funcHasDefer // contains a defer statement
411-
funcNilCheckDisabled // disable nil checks when compiling this function
409+
funcNoFramePointer // Must not use a frame pointer for this function
410+
funcHasDefer // contains a defer statement
411+
funcNilCheckDisabled // disable nil checks when compiling this function
412+
funcInlinabilityChecked // inliner has already determined whether the function is inlinable
412413
)
413414

414-
func (f *Func) Dupok() bool { return f.flags&funcDupok != 0 }
415-
func (f *Func) Wrapper() bool { return f.flags&funcWrapper != 0 }
416-
func (f *Func) Needctxt() bool { return f.flags&funcNeedctxt != 0 }
417-
func (f *Func) ReflectMethod() bool { return f.flags&funcReflectMethod != 0 }
418-
func (f *Func) IsHiddenClosure() bool { return f.flags&funcIsHiddenClosure != 0 }
419-
func (f *Func) NoFramePointer() bool { return f.flags&funcNoFramePointer != 0 }
420-
func (f *Func) HasDefer() bool { return f.flags&funcHasDefer != 0 }
421-
func (f *Func) NilCheckDisabled() bool { return f.flags&funcNilCheckDisabled != 0 }
422-
423-
func (f *Func) SetDupok(b bool) { f.flags.set(funcDupok, b) }
424-
func (f *Func) SetWrapper(b bool) { f.flags.set(funcWrapper, b) }
425-
func (f *Func) SetNeedctxt(b bool) { f.flags.set(funcNeedctxt, b) }
426-
func (f *Func) SetReflectMethod(b bool) { f.flags.set(funcReflectMethod, b) }
427-
func (f *Func) SetIsHiddenClosure(b bool) { f.flags.set(funcIsHiddenClosure, b) }
428-
func (f *Func) SetNoFramePointer(b bool) { f.flags.set(funcNoFramePointer, b) }
429-
func (f *Func) SetHasDefer(b bool) { f.flags.set(funcHasDefer, b) }
430-
func (f *Func) SetNilCheckDisabled(b bool) { f.flags.set(funcNilCheckDisabled, b) }
415+
func (f *Func) Dupok() bool { return f.flags&funcDupok != 0 }
416+
func (f *Func) Wrapper() bool { return f.flags&funcWrapper != 0 }
417+
func (f *Func) Needctxt() bool { return f.flags&funcNeedctxt != 0 }
418+
func (f *Func) ReflectMethod() bool { return f.flags&funcReflectMethod != 0 }
419+
func (f *Func) IsHiddenClosure() bool { return f.flags&funcIsHiddenClosure != 0 }
420+
func (f *Func) NoFramePointer() bool { return f.flags&funcNoFramePointer != 0 }
421+
func (f *Func) HasDefer() bool { return f.flags&funcHasDefer != 0 }
422+
func (f *Func) NilCheckDisabled() bool { return f.flags&funcNilCheckDisabled != 0 }
423+
func (f *Func) InlinabilityChecked() bool { return f.flags&funcInlinabilityChecked != 0 }
424+
425+
func (f *Func) SetDupok(b bool) { f.flags.set(funcDupok, b) }
426+
func (f *Func) SetWrapper(b bool) { f.flags.set(funcWrapper, b) }
427+
func (f *Func) SetNeedctxt(b bool) { f.flags.set(funcNeedctxt, b) }
428+
func (f *Func) SetReflectMethod(b bool) { f.flags.set(funcReflectMethod, b) }
429+
func (f *Func) SetIsHiddenClosure(b bool) { f.flags.set(funcIsHiddenClosure, b) }
430+
func (f *Func) SetNoFramePointer(b bool) { f.flags.set(funcNoFramePointer, b) }
431+
func (f *Func) SetHasDefer(b bool) { f.flags.set(funcHasDefer, b) }
432+
func (f *Func) SetNilCheckDisabled(b bool) { f.flags.set(funcNilCheckDisabled, b) }
433+
func (f *Func) SetInlinabilityChecked(b bool) { f.flags.set(funcInlinabilityChecked, b) }
431434

432435
type Op uint8
433436

test/escape4.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ func f1() {
2222

2323
// Escape analysis used to miss inlined code in closures.
2424

25-
func() { // ERROR "func literal does not escape" "can inline f1.func1"
26-
p = alloc(3) // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"
27-
}()
25+
func() { // ERROR "can inline f1.func1"
26+
p = alloc(3) // ERROR "inlining call to alloc"
27+
}() // ERROR "inlining call to f1.func1" "inlining call to alloc" "&x escapes to heap" "moved to heap: x"
2828

2929
f = func() { // ERROR "func literal escapes to heap" "can inline f1.func2"
3030
p = alloc(3) // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"

test/inline.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
package foo
1111

12-
import "unsafe"
12+
import (
13+
"errors"
14+
"unsafe"
15+
)
1316

1417
func add2(p *byte, n uintptr) *byte { // ERROR "can inline add2" "leaking param: p to result"
1518
return (*byte)(add1(unsafe.Pointer(p), n)) // ERROR "inlining call to add1"
@@ -46,6 +49,50 @@ func j(x int) int { // ERROR "can inline j"
4649
}
4750
}
4851

52+
var somethingWrong error = errors.New("something went wrong")
53+
54+
// local closures can be inlined
55+
func l(x, y int) (int, int, error) {
56+
e := func(err error) (int, int, error) { // ERROR "can inline l.func1" "func literal does not escape" "leaking param: err to result"
57+
return 0, 0, err
58+
}
59+
if x == y {
60+
e(somethingWrong) // ERROR "inlining call to l.func1"
61+
}
62+
return y, x, nil
63+
}
64+
65+
// any re-assignment prevents closure inlining
66+
func m() int {
67+
foo := func() int { return 1 } // ERROR "can inline m.func1" "func literal does not escape"
68+
x := foo()
69+
foo = func() int { return 2 } // ERROR "can inline m.func2" "func literal does not escape"
70+
return x + foo()
71+
}
72+
73+
// address taking prevents closure inlining
74+
func n() int {
75+
foo := func() int { return 1 } // ERROR "can inline n.func1" "func literal does not escape"
76+
bar := &foo // ERROR "&foo does not escape"
77+
x := (*bar)() + foo()
78+
return x
79+
}
80+
81+
// make sure assignment inside closure is detected
82+
func o() int {
83+
foo := func() int { return 1 } // ERROR "can inline o.func1" "func literal does not escape"
84+
func(x int) { // ERROR "func literal does not escape"
85+
if x > 10 {
86+
foo = func() int { return 2 } // ERROR "can inline o.func2" "func literal escapes"
87+
}
88+
}(11)
89+
return foo()
90+
}
91+
92+
func p() int {
93+
return func() int { return 42 }() // ERROR "can inline p.func1" "inlining call to p.func1"
94+
}
95+
4996
// can't currently inline functions with a break statement
5097
func switchBreak(x, y int) int {
5198
var n int

0 commit comments

Comments
 (0)