内联优化背景:
最近使用 onec.Do 对应 sync 的 once.go 文件的源码,发现在最近一年内发生了不小的变动,而且这模式的变动在多处被以相同的方法进行了优化,就是函数内联优化。
源码网页出处:
sync: allow inlining the Once.Do fast path
该 commit 对应的 Reviewed-on 地址:
https://go-review.googlesource.com/c/go/+/152697
为何要将内部函数拆分出来,核心思想就是 简化 Do 这个函数,使其在编译器自动函数内联优化的时候 能够优化到这个函数。
拆开后 会触发 栈中内联。Mid-stack inlining in Go
如果不拆分出来,由于 limit inlining 该函数就会被限制住,不会被触发内联。
具体内容:
//go:noinline
func max(a, b int) int {
if a > b {
return a
}
return b
}
var Result int
func BenchmarkMax(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = max(-1, 1)
}
Result = r
}
% go test -run=none -bench=BenchmarkMax
goos: darwin
goarch: amd64
pkg: gitlab.topstore.cn/saas-micro/agents/internal/notification/services
BenchmarkMax-4 462781717 2.54 ns/op
PASS
ok gitlab.topstore.cn/saas-micro/agents/internal/notification/services 2.461s
然后我们去掉: //go:noinline
go test -run=none -bench=BenchmarkMax
goos: darwin
goarch: amd64
pkg: gitlab.topstore.cn/saas-micro/agents/internal/notification/services
BenchmarkMax-4 1000000000 0.362 ns/op
PASS
ok gitlab.topstore.cn/saas-micro/agents/internal/notification/services 0.419s
这里只是我们一次的压测结果,我这里想多次几次,比如压测10次再来看看对比结果。
让我们来直接用 benchstat 命令 进行多次压测比较下两次新旧测试性能的对比结果:
go test -run=none -bench=BenchmarkMax -count=10 > old.txt
运行出来的 old.txt (禁止局部函数内联压测版)文件内容如下:
goos: darwin
goarch: amd64
pkg: gitlab.topstore.cn/saas-micro/agents/internal/notification/services
BenchmarkMax-4 456073288 2.72 ns/op
BenchmarkMax-4 370783882 6.16 ns/op
BenchmarkMax-4 422050999 2.58 ns/op
BenchmarkMax-4 451509020 3.36 ns/op
BenchmarkMax-4 409592560 2.94 ns/op
BenchmarkMax-4 455027424 3.16 ns/op
BenchmarkMax-4 416430606 4.38 ns/op
BenchmarkMax-4 335487448 3.32 ns/op
BenchmarkMax-4 355569391 3.24 ns/op
BenchmarkMax-4 357870064 3.42 ns/op
PASS
ok gitlab.topstore.cn/saas-micro/agents/internal/notification/services 17.209s
下面是 启动函数内联版:
go test -run=none -bench=BenchmarkMax -count=10 > new.txt
具体内容如下:
goos: darwin
goarch: amd64
pkg: gitlab.topstore.cn/saas-micro/agents/internal/notification/services
BenchmarkMax-4 1000000000 0.821 ns/op
BenchmarkMax-4 1000000000 0.386 ns/op
BenchmarkMax-4 1000000000 0.372 ns/op
BenchmarkMax-4 1000000000 0.356 ns/op
BenchmarkMax-4 1000000000 0.394 ns/op
BenchmarkMax-4 1000000000 0.374 ns/op
BenchmarkMax-4 1000000000 0.410 ns/op
BenchmarkMax-4 1000000000 0.359 ns/op
BenchmarkMax-4 1000000000 0.359 ns/op
BenchmarkMax-4 1000000000 0.355 ns/op
PASS
ok gitlab.topstore.cn/saas-micro/agents/internal/notification/services 4.677s
最后将两个txt文件进行对比:
benchstat 可多次基准测试并对比的工具
先go install 下这个 benchstat 工具
go install golang.org/x/perf/cmd/benchstat
运行如下命令:
benchstat {old,new}.txt
结果如下:
name old time/op new time/op delta
Max-4 3.09ns ±17% 0.37ns ±10% -87.91% (p=0.000 n=8+9)
嗯,很明显编译时 不要禁止函数自动内联性能高很多。
那如果我不把函数拆分,直接手动内联结果会是如何的呢?
手动内联版本1:
func BenchmarkMax(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
if -1 > i {
r = -1
} else {
r = i
}
}
Result = r
}
手动内联版本2:
func BenchmarkMax(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
if false {
r = -1
} else {
r = i
}
}
Result = r
}
手动内联最终版:
func BenchmarkMax(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = i
}
Result = r
}
参考:
下面两篇英文原文都出自 Dave Cheney:
注意:下面这篇文章是关键,好好理解,就明白开篇提到的结论了:
Mid-stack inlining in the Go compiler