Skip to content

Commit c4b65fa

Browse files
huguesbmdempsky
authored andcommitted
cmd/compile: inline closures with captures
When inlining a closure with captured variables, walk up the param chain to find the one that is defined inside the scope into which the function is being inlined, and map occurrences of the captures to temporary inlvars, similarly to what is done for function parameters. No noticeable impact on compilation speed and binary size. Minor improvements to go1 benchmarks on darwin/amd64 name old time/op new time/op delta BinaryTree17-4 2.59s ± 3% 2.58s ± 1% ~ (p=0.470 n=19+19) Fannkuch11-4 3.15s ± 2% 3.15s ± 1% ~ (p=0.647 n=20+19) FmtFprintfEmpty-4 43.7ns ± 3% 43.4ns ± 4% ~ (p=0.178 n=18+20) FmtFprintfString-4 74.0ns ± 2% 77.1ns ± 7% +4.13% (p=0.000 n=20+20) FmtFprintfInt-4 77.2ns ± 3% 79.2ns ± 6% +2.53% (p=0.000 n=20+20) FmtFprintfIntInt-4 112ns ± 4% 112ns ± 2% ~ (p=0.672 n=20+19) FmtFprintfPrefixedInt-4 136ns ± 1% 135ns ± 2% ~ (p=0.827 n=16+20) FmtFprintfFloat-4 232ns ± 2% 233ns ± 1% ~ (p=0.194 n=20+20) FmtManyArgs-4 490ns ± 2% 484ns ± 2% -1.28% (p=0.001 n=20+20) GobDecode-4 6.68ms ± 2% 6.72ms ± 2% ~ (p=0.113 n=20+19) GobEncode-4 5.62ms ± 2% 5.71ms ± 2% +1.64% (p=0.000 n=20+19) Gzip-4 235ms ± 3% 236ms ± 2% ~ (p=0.607 n=20+19) Gunzip-4 37.1ms ± 2% 36.8ms ± 3% ~ (p=0.060 n=20+20) HTTPClientServer-4 61.9µs ± 2% 62.7µs ± 4% +1.24% (p=0.007 n=18+19) JSONEncode-4 12.5ms ± 2% 12.4ms ± 3% ~ (p=0.192 n=20+20) JSONDecode-4 51.6ms ± 3% 51.0ms ± 3% -1.19% (p=0.008 n=20+19) Mandelbrot200-4 4.12ms ± 6% 4.06ms ± 5% ~ (p=0.063 n=20+20) GoParse-4 3.12ms ± 5% 3.10ms ± 2% ~ (p=0.402 n=19+19) RegexpMatchEasy0_32-4 80.7ns ± 2% 75.1ns ± 9% -6.94% (p=0.000 n=17+20) RegexpMatchEasy0_1K-4 197ns ± 2% 186ns ± 2% -5.43% (p=0.000 n=20+20) RegexpMatchEasy1_32-4 77.5ns ± 4% 71.9ns ± 7% -7.25% (p=0.000 n=20+18) RegexpMatchEasy1_1K-4 341ns ± 3% 341ns ± 3% ~ (p=0.732 n=20+20) RegexpMatchMedium_32-4 113ns ± 2% 112ns ± 3% ~ (p=0.102 n=20+20) RegexpMatchMedium_1K-4 36.6µs ± 2% 35.8µs ± 2% -2.26% (p=0.000 n=18+20) RegexpMatchHard_32-4 1.75µs ± 3% 1.74µs ± 2% ~ (p=0.473 n=20+19) RegexpMatchHard_1K-4 52.6µs ± 2% 52.0µs ± 3% -1.15% (p=0.005 n=20+20) Revcomp-4 381ms ± 4% 377ms ± 2% ~ (p=0.067 n=20+18) Template-4 57.3ms ± 2% 57.7ms ± 2% ~ (p=0.108 n=20+20) TimeParse-4 291ns ± 3% 292ns ± 2% ~ (p=0.585 n=20+20) TimeFormat-4 314ns ± 3% 315ns ± 1% ~ (p=0.681 n=20+20) [Geo mean] 47.4µs 47.1µs -0.73% name old speed new speed delta GobDecode-4 115MB/s ± 2% 114MB/s ± 2% ~ (p=0.115 n=20+19) GobEncode-4 137MB/s ± 2% 134MB/s ± 2% -1.63% (p=0.000 n=20+19) Gzip-4 82.5MB/s ± 3% 82.4MB/s ± 2% ~ (p=0.612 n=20+19) Gunzip-4 523MB/s ± 2% 528MB/s ± 3% ~ (p=0.060 n=20+20) JSONEncode-4 155MB/s ± 2% 156MB/s ± 3% ~ (p=0.192 n=20+20) JSONDecode-4 37.6MB/s ± 3% 38.1MB/s ± 3% +1.21% (p=0.007 n=20+19) GoParse-4 18.6MB/s ± 4% 18.7MB/s ± 2% ~ (p=0.405 n=19+19) RegexpMatchEasy0_32-4 396MB/s ± 2% 426MB/s ± 8% +7.56% (p=0.000 n=17+20) RegexpMatchEasy0_1K-4 5.18GB/s ± 2% 5.48GB/s ± 2% +5.79% (p=0.000 n=20+20) RegexpMatchEasy1_32-4 413MB/s ± 4% 444MB/s ± 6% +7.46% (p=0.000 n=20+19) RegexpMatchEasy1_1K-4 3.00GB/s ± 3% 3.00GB/s ± 3% ~ (p=0.678 n=20+20) RegexpMatchMedium_32-4 8.82MB/s ± 2% 8.90MB/s ± 3% +0.99% (p=0.044 n=20+20) RegexpMatchMedium_1K-4 28.0MB/s ± 2% 28.6MB/s ± 2% +2.32% (p=0.000 n=18+20) RegexpMatchHard_32-4 18.3MB/s ± 3% 18.4MB/s ± 2% ~ (p=0.482 n=20+19) RegexpMatchHard_1K-4 19.5MB/s ± 2% 19.7MB/s ± 3% +1.18% (p=0.004 n=20+20) Revcomp-4 668MB/s ± 4% 674MB/s ± 2% ~ (p=0.066 n=20+18) Template-4 33.8MB/s ± 2% 33.6MB/s ± 2% ~ (p=0.104 n=20+20) [Geo mean] 124MB/s 126MB/s +1.54% Updates #15561 Updates #18270 Change-Id: I980086efe28b36aa27f81577065e2a729ff03d4e Reviewed-on: https://go-review.googlesource.com/72490 Reviewed-by: Hugues Bruant <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]> Run-TryBot: Daniel Martí <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 936b977 commit c4b65fa

File tree

4 files changed

+208
-14
lines changed

4 files changed

+208
-14
lines changed

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

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@ func (v *hairyVisitor) visit(n *Node) bool {
280280
v.budget -= fn.InlCost
281281
break
282282
}
283+
if n.Left.Op == OCLOSURE {
284+
if fn := inlinableClosure(n.Left); fn != nil {
285+
v.budget -= fn.Func.InlCost
286+
break
287+
}
288+
} else if n.Left.Op == ONAME && n.Left.Name != nil && n.Left.Name.Defn != nil {
289+
// NB: this case currently cannot trigger since closure definition
290+
// prevents inlining
291+
// NB: ideally we would also handle captured variables defined as
292+
// closures in the outer scope this brings us back to the idea of
293+
// function value propagation, which if available would both avoid
294+
// the "reassigned" check and neatly handle multiple use cases in a
295+
// single code path
296+
if d := n.Left.Name.Defn; d.Op == OAS && d.Right.Op == OCLOSURE {
297+
if fn := inlinableClosure(d.Right); fn != nil {
298+
v.budget -= fn.Func.InlCost
299+
break
300+
}
301+
}
302+
}
283303

284304
if n.Left.isMethodExpression() {
285305
if d := asNode(n.Left.Sym.Def); d != nil && d.Func.Inl.Len() != 0 {
@@ -629,22 +649,16 @@ func inlnode(n *Node) *Node {
629649
return n
630650
}
631651

652+
// inlinableClosure takes an OCLOSURE node and follows linkage to the matching ONAME with
653+
// the inlinable body. Returns nil if the function is not inlinable.
632654
func inlinableClosure(n *Node) *Node {
633655
c := n.Func.Closure
634656
caninl(c)
635657
f := c.Func.Nname
636-
if f != nil && f.Func.Inl.Len() != 0 {
637-
if n.Func.Cvars.Len() != 0 {
638-
// FIXME: support closure with captured variables
639-
// they currently result in invariant violation in the SSA phase
640-
if Debug['m'] > 1 {
641-
fmt.Printf("%v: cannot inline closure w/ captured vars %v\n", n.Line(), n.Left)
642-
}
643-
return nil
644-
}
645-
return f
658+
if f == nil || f.Func.Inl.Len() == 0 {
659+
return nil
646660
}
647-
return nil
661+
return f
648662
}
649663

650664
// reassigned takes an ONAME node, walks the function in which it is defined, and returns a boolean
@@ -792,16 +806,53 @@ func mkinlcall1(n, fn *Node, isddd bool) *Node {
792806

793807
ninit := n.Ninit
794808

809+
// Make temp names to use instead of the originals.
810+
inlvars := make(map[*Node]*Node)
811+
795812
// Find declarations corresponding to inlineable body.
796813
var dcl []*Node
797814
if fn.Name.Defn != nil {
798815
dcl = fn.Func.Inldcl.Slice() // local function
816+
817+
// handle captured variables when inlining closures
818+
if c := fn.Name.Defn.Func.Closure; c != nil {
819+
for _, v := range c.Func.Cvars.Slice() {
820+
if v.Op == OXXX {
821+
continue
822+
}
823+
824+
o := v.Name.Param.Outer
825+
// make sure the outer param matches the inlining location
826+
// NB: if we enabled inlining of functions containing OCLOSURE or refined
827+
// the reassigned check via some sort of copy propagation this would most
828+
// likely need to be changed to a loop to walk up to the correct Param
829+
if o == nil || (o.Name.Curfn != Curfn && o.Name.Curfn.Func.Closure != Curfn) {
830+
Fatalf("%v: unresolvable capture %v %v\n", n.Line(), fn, v)
831+
}
832+
833+
if v.Name.Byval() {
834+
iv := typecheck(inlvar(v), Erv)
835+
ninit.Append(nod(ODCL, iv, nil))
836+
ninit.Append(typecheck(nod(OAS, iv, o), Etop))
837+
inlvars[v] = iv
838+
} else {
839+
addr := newname(lookup("&" + v.Sym.Name))
840+
addr.Type = types.NewPtr(v.Type)
841+
ia := typecheck(inlvar(addr), Erv)
842+
ninit.Append(nod(ODCL, ia, nil))
843+
ninit.Append(typecheck(nod(OAS, ia, nod(OADDR, o, nil)), Etop))
844+
inlvars[addr] = ia
845+
846+
// When capturing by reference, all occurrence of the captured var
847+
// must be substituted with dereference of the temporary address
848+
inlvars[v] = typecheck(nod(OIND, ia, nil), Erv)
849+
}
850+
}
851+
}
799852
} else {
800853
dcl = fn.Func.Dcl // imported function
801854
}
802855

803-
// Make temp names to use instead of the originals.
804-
inlvars := make(map[*Node]*Node)
805856
for _, ln := range dcl {
806857
if ln.Op != ONAME {
807858
continue

test/closure3.dir/main.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,114 @@ func main() {
170170
}
171171
}()
172172
}
173+
174+
{
175+
x := 42
176+
if y := func() int { // ERROR "can inline main.func20"
177+
return x
178+
}(); y != 42 { // ERROR "inlining call to main.func20"
179+
panic("y != 42")
180+
}
181+
if y := func() int { // ERROR "can inline main.func21" "func literal does not escape"
182+
return x
183+
}; y() != 42 { // ERROR "inlining call to main.func21"
184+
panic("y() != 42")
185+
}
186+
}
187+
188+
{
189+
x := 42
190+
if z := func(y int) int { // ERROR "func literal does not escape"
191+
return func() int { // ERROR "can inline main.func22.1"
192+
return x + y
193+
}() // ERROR "inlining call to main.func22.1"
194+
}(1); z != 43 {
195+
panic("z != 43")
196+
}
197+
if z := func(y int) int { // ERROR "func literal does not escape"
198+
return func() int { // ERROR "can inline main.func23.1"
199+
return x + y
200+
}() // ERROR "inlining call to main.func23.1"
201+
}; z(1) != 43 {
202+
panic("z(1) != 43")
203+
}
204+
}
205+
206+
{
207+
a := 1
208+
func() { // ERROR "func literal does not escape"
209+
func() { // ERROR "can inline main.func24"
210+
a = 2
211+
}() // ERROR "inlining call to main.func24" "&a does not escape"
212+
}()
213+
if a != 2 {
214+
panic("a != 2")
215+
}
216+
}
217+
218+
{
219+
b := 2
220+
func(b int) { // ERROR "func literal does not escape"
221+
func() { // ERROR "can inline main.func25.1"
222+
b = 3
223+
}() // ERROR "inlining call to main.func25.1" "&b does not escape"
224+
if b != 3 {
225+
panic("b != 3")
226+
}
227+
}(b)
228+
if b != 2 {
229+
panic("b != 2")
230+
}
231+
}
232+
233+
{
234+
c := 3
235+
func() { // ERROR "func literal does not escape"
236+
c = 4
237+
func() { // ERROR "func literal does not escape"
238+
if c != 4 {
239+
panic("c != 4")
240+
}
241+
}()
242+
}()
243+
if c != 4 {
244+
panic("c != 4")
245+
}
246+
}
247+
248+
{
249+
a := 2
250+
if r := func(x int) int { // ERROR "func literal does not escape"
251+
b := 3
252+
return func(y int) int { // ERROR "func literal does not escape"
253+
c := 5
254+
return func(z int) int { // ERROR "can inline main.func27.1.1"
255+
return a*x + b*y + c*z
256+
}(10) // ERROR "inlining call to main.func27.1.1"
257+
}(100)
258+
}(1000); r != 2350 {
259+
panic("r != 2350")
260+
}
261+
}
262+
263+
{
264+
a := 2
265+
if r := func(x int) int { // ERROR "func literal does not escape"
266+
b := 3
267+
return func(y int) int { // ERROR "func literal does not escape"
268+
c := 5
269+
func(z int) { // ERROR "can inline main.func28.1.1"
270+
a = a * x
271+
b = b * y
272+
c = c * z
273+
}(10) // ERROR "inlining call to main.func28.1.1" "&a does not escape" "&b does not escape" "&c does not escape"
274+
return a + c
275+
}(100) + b
276+
}(1000); r != 2350 {
277+
panic("r != 2350")
278+
}
279+
if a != 2000 {
280+
panic("a != 2000")
281+
}
282+
}
173283
}

test/fixedbugs/issue20250.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// errorcheck -0 -live -d=compilelater,eagerwb
1+
// errorcheck -0 -live -l -d=compilelater,eagerwb
22

33
// Copyright 2017 The Go Authors. All rights reserved.
44
// Use of this source code is governed by a BSD-style

test/inline.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ func p() int {
9393
return func() int { return 42 }() // ERROR "can inline p.func1" "inlining call to p.func1"
9494
}
9595

96+
func q(x int) int {
97+
foo := func() int { return x * 2 } // ERROR "can inline q.func1" "q func literal does not escape"
98+
return foo() // ERROR "inlining call to q.func1"
99+
}
100+
101+
func r(z int) int {
102+
foo := func(x int) int { // ERROR "can inline r.func1" "r func literal does not escape"
103+
return x + z
104+
}
105+
bar := func(x int) int { // ERROR "r func literal does not escape"
106+
return x + func(y int) int { // ERROR "can inline r.func2.1"
107+
return 2*y + x*z
108+
}(x) // ERROR "inlining call to r.func2.1"
109+
}
110+
return foo(42) + bar(42) // ERROR "inlining call to r.func1"
111+
}
112+
113+
func s0(x int) int {
114+
foo := func() { // ERROR "can inline s0.func1" "s0 func literal does not escape"
115+
x = x + 1
116+
}
117+
foo() // ERROR "inlining call to s0.func1" "&x does not escape"
118+
return x
119+
}
120+
121+
func s1(x int) int {
122+
foo := func() int { // ERROR "can inline s1.func1" "s1 func literal does not escape"
123+
return x
124+
}
125+
x = x + 1
126+
return foo() // ERROR "inlining call to s1.func1" "&x does not escape"
127+
}
128+
96129
// can't currently inline functions with a break statement
97130
func switchBreak(x, y int) int {
98131
var n int

0 commit comments

Comments
 (0)