函数内联
函数内联是编译器的一种优化技术,它是指将较小的函数直接组合到调用者的函数中,这样可以减少函数调用带来的开销,不过这样编译出来的二进制体积会稍大一些。
在Go语言中,在函数前加上//go: noinline指令,就可以在编译的时候禁止对函数内联优化。
示例
下面是一个基准测试,对比下函数有没有被内联性能差距:
1
2
3
4
5
6
7
8
9
10
11
12
//go:noinline
func max(a, b int) int {
if a > b {
return a
}
return b
}
func BenchmarkTest_max(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = max(i, 1)
}
}
函数没有被内联的测试结果(noinline):
1
2
3
4
5
6
7
8
goos: darwin
goarch: amd64
pkg: examples/func/inline
cpu: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
BenchmarkTest_max
BenchmarkTest_max-8 685833063 1.754 ns/op 0 B/op 0 allocs/op
PASS
ok examples/func/inline 1.669s
然后去掉//go:noinline指令后就是函数被内联的测试结果:
1
2
3
4
5
6
7
8
goos: darwin
goarch: amd64
pkg: examples/func/inline
cpu: Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
BenchmarkTest_max
BenchmarkTest_max-8 1000000000 0.2923 ns/op 0 B/op 0 allocs/op
PASS
ok examples/func/inline 3.361s
可以看到这个max函数被内联后性能提升了将近5倍多。
不过并不是所有的函数都会进行内联,Go的编译器会计算出函数内联的花费成本,所以只有操作相对简单的函数才会进行内联。如果函数内部有 for、rang、go、select等语句或函数内部有较复杂的操作时,该函数就不会被内联,比如递归函数。
除了添加noinline指令,还可以在编译时增加增加编译器选项参数-l禁止内联,不过这样会对程序中所有的函数禁止内联:
1
2
go build -gcflags="-l" main.go
go tool compile -l main.go
那么如何查看一个函数是否可以被内联呢?可以在程序编译的时候增加-m=2参数,根据打印出的编译信息,可以看出该函数是否被内联了,以及不能内联的原因是什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import "fmt"
func max(a, b int) int {
if a > b {
return a
}
return b
}
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
r := max(1, 2)
fmt.Println(r)
r = fib(5)
fmt.Println(r)
}
对上面的代码进行编译:
1
2
3
4
5
6
go tool compile -m=2 main.go | grep inline
//Output
main.go:5:6: can inline max with cost 8 as: func(int, int) int { if a > b { return a }; return b }
main.go:11:6: cannot inline fib: recursive
main.go:17:6: cannot inline main: function too complex: cost 230 exceeds budget 80
根据输出结果可以看到max函数可以被内联,而fib函数由于是一个递归函数不能被内联。