9

Golang函数内联优化

 3 years ago
source link: https://driverzhang.github.io/post/golang%E5%87%BD%E6%95%B0%E5%86%85%E8%81%94%E4%BC%98%E5%8C%96/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

内联优化背景:

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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK