Golang函数内联优化

内联优化背景:

最近使用 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:

Inlining optimisations in Go

函数内联优化 中文版

Mid-stack inlining in Go

Go 中对栈中函数进行内联 中文版

注意:下面这篇文章是关键,好好理解,就明白开篇提到的结论了:

Mid-stack inlining in the Go compiler