Can a lambda passed to other functions be inlined by a Go compiler?

169 views
Skip to first unread message

Shamil Mukhetdinov

unread,
Oct 23, 2023, 12:19:51 PM10/23/23
to golang-nuts
Imagine you have something like this:

func foo(data []int, suitable func(v int) bool) []int 
   result := make([]int, 0, len(data) 
    for _, v := range data { 
        if suitable(v) { 
            result = append(result, v) 
        } 
    } return result 



func main()
    data := []int{1, 2, 3, 4, 5
    foo(data, func(v int) bool { return v % 2 == 0 }) 
}

If instead of suitable lambda I use an explicit function call, that call may be inlined in Go.

But C++ supporting inlining for copied closures.

Does golang support inlining for lambdas, if I have that function signature? Or only non-escaping to the other functions lambdas can be inlined?

I found this issue which is still open

drc...@google.com

unread,
Oct 25, 2023, 6:15:09 PM10/25/23
to golang-nuts
The short answer is yes, but not in this example.

In your example the inline doesn't happen because `foo` is (barely) too complex so it is not inlined and the closure remains a parameter:
```
go build -gcflags=-m=2 main.go
# command-line-arguments
./main.go:3:6: cannot inline foo: function too complex: cost 82 exceeds budget 80
./main.go:13:6: cannot inline main: function too complex: cost 87 exceeds budget 80
./main.go:15:12: can inline main.func1 with cost 6 as: func(int) bool { return v % 2 == 0 }
./main.go:4:16: make([]int, 0, len(data)) escapes to heap:
./main.go:4:16:   flow: {heap} = &{storage for make([]int, 0, len(data))}:
./main.go:4:16:     from make([]int, 0, len(data)) (non-constant size) at ./main.go:4:16
./main.go:3:10: data does not escape
./main.go:3:22: suitable does not escape
./main.go:4:16: make([]int, 0, len(data)) escapes to heap
./main.go:14:15: []int{...} does not escape
./main.go:15:12: func literal does not escape
```

I simplified your `foo` to take result as a parameter, and that did the job:
```
go build -gcflags=-m=2 main.go
# command-line-arguments
./main.go:3:6: can inline foo with cost 74 as: func([]int, []int, func(int) bool) []int { for loop; return result }
./main.go:12:6: cannot inline main: function too complex: cost 113 exceeds budget 80
./main.go:15:20: can inline main.func1 with cost 6 as: func(int) bool { return v % 2 == 0 }
./main.go:15:5: inlining call to foo
./main.go:15:5: inlining call to main.func1
./main.go:3:16: parameter result leaks to ~r0 with derefs=0:
./main.go:3:16:   flow: ~r0 = result:
./main.go:3:16:     from return result (return) at ./main.go:9:2
./main.go:3:10: data does not escape
./main.go:3:16: leaking param: result to result ~r0 level=0
./main.go:3:30: suitable does not escape
./main.go:14:16: make([]int, 0, len(data)) escapes to heap:
./main.go:14:16:   flow: {heap} = &{storage for make([]int, 0, len(data))}:
./main.go:14:16:     from make([]int, 0, len(data)) (non-constant size) at ./main.go:14:16
./main.go:13:15: []int{...} does not escape
./main.go:14:16: make([]int, 0, len(data)) escapes to heap
./main.go:15:20: func literal does not escape
```
You can examine the output either with `-S`
```
go build -gcflags=-S main.go > main.asm
```
or with `GOSSAFUNC=main`
```
GOSSAFUNC=main go build main.go
# runtime
dumped SSA for main,1 to ../../src/inline-lam/ssa.html
# command-line-arguments
dumped SSA for main,1 to ./ssa.html
```
and in either case you will see that the call to foo and the closure are gone.

With PGO ( https://go.dev/doc/pgo ) if that function (foo) is on a hot path, it is more likely to get inlined and thus allow the closure inlining.
(and if it's not on a hot path, do you care?)
Reply all
Reply to author
Forward
0 new messages