乐趣区

关于go:Go十大常见错误第2篇benchmark性能测试的坑

前言

这是 Go 十大常见谬误系列的第二篇:benchmark 性能测试的坑。素材来源于 Go 布道者,现 Docker 公司资深工程师 Teiva Harsanyi。

本文波及的源代码全副开源在:Go 十大常见谬误源代码,欢送大家关注公众号,及时获取本系列最新更新。

场景

go test反对 benchmark 性能测试,然而你晓得这里可能有坑么?

一个常见的坑是编译器内联优化,咱们来看一个具体的例子:

func add(a int, b int) int {return a + b}

当初咱们要对 add 函数做性能测试,可能会编写如下测试代码:

func BenchmarkWrong(b *testing.B) {b.ResetTimer()
    for i := 0; i < b.N; i++ {add(1000000000, 1000000001)
    }
}

这里可能有什么坑呢?对于编译器而言,add函数是一个叶子函数 (leaf function),即add 函数自身没有调用其它函数,所以编译器会对 add 函数的调用做内联 (inline) 优化,这会导致性能测试的后果不精确。因为咱们通常要测试的是本人程序自身的执行效率,而不是编译器做了优化后的执行效率,这样才不便咱们对程序的性能有一个正确的认知,而且你做 go test 测试时编译器的优化成果和理论生产环境运行时编译器的优化成果可能也不一样

那怎么晓得执行 go test 的时候编译器是否做了内联优化呢?很简略,给 go test 减少 -gcflags="-m" 参数,-m示意打印编译器做出的优化决定。

$ go test -gcflags="-m" -v -bench=BenchmarkWrong -count 1
# example.com/benchmark [example.com/benchmark.test]
./go_util.go:3:6: can inline add
./go_bench_test.go:19:6: inlining call to add
./go_bench_test.go:16:21: b does not escape
# example.com/benchmark.test
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build2365344599/b001/_testmain.go:33:6: can inline init.0
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build2365344599/b001/_testmain.go:41:24: inlining call to testing.MainStart
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build2365344599/b001/_testmain.go:41:42: testdeps.TestDeps{} escapes to heap
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build2365344599/b001/_testmain.go:41:24: &testing.M{...} escapes to heap
goos: darwin
goarch: amd64
pkg: example.com/benchmark
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkWrong
BenchmarkWrong-4        1000000000               0.4601 ns/op
PASS
ok      example.com/benchmark   0.605s

下面的执行后果的 ./go_bench_test.go:19:6: inlining call to add 就示意编译器对 BenchmarkWrong 里的 add 函数调用做了内联优化。

备注 : -gcflags 的所有参数值能够执行go tool compile --help 进行查看。

最佳实际

那在性能测试的时候怎么禁用编译期的内联优化呢?有 2 个计划:

-gcflags=”-l”

第一种计划,执行 go test 的时候,减少 -gcfloags="-l" 参数,-l示意禁用编译器的内联优化。

$ go test -gcflags="-m -l" -v -bench=BenchmarkWrong -count 3
# example.com/benchmark [example.com/benchmark.test]
./go_bench_test.go:16:21: b does not escape
# example.com/benchmark.test
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build2785655381/b001/_testmain.go:41:42: testdeps.TestDeps{} escapes to heap
goos: darwin
goarch: amd64
pkg: example.com/benchmark
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkWrong
BenchmarkWrong-4        476215998                2.447 ns/op
BenchmarkWrong-4        492860170                2.404 ns/op
BenchmarkWrong-4        483547294                2.388 ns/op
PASS
ok      example.com/benchmark   4.568s

通过下面的输入后果能够看出,并没有 inlining call 字样,这就证实了应用 -gcflags="-l" 参数后,编译器没有做内联优化了。

比照下编译期内联优化禁用前后的后果,性能差了将近 5 倍。

  • 开启内联优化,耗时:0.4601 ns/op
  • -gcflags="-l"敞开内联优化,耗时大略:2.4 ns/op

go:noinline

第二种计划,应用 //go:noinline 编译器指令(compiler directive),编译器在编译时会辨认到这个指令,不做内联优化。

//go:noinline
func add(a int, b int) int {return a + b}

通过这种形式批改代码后,咱们就不须要应用 -gcflags="-l" 参数了,咱们来看看性能测试后果:

$ go test -gcflags="-m" -v -bench=BenchmarkWrong -count 3
# example.com/benchmark [example.com/benchmark.test]
./go_bench_test.go:16:21: b does not escape
# example.com/benchmark.test
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build1050705055/b001/_testmain.go:33:6: can inline init.0
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build1050705055/b001/_testmain.go:41:24: inlining call to testing.MainStart
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build1050705055/b001/_testmain.go:41:42: testdeps.TestDeps{} escapes to heap
/var/folders/pv/_x849j6n22x37xxd9cstgwkr0000gn/T/go-build1050705055/b001/_testmain.go:41:24: &testing.M{...} escapes to heap
goos: darwin
goarch: amd64
pkg: example.com/benchmark
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
BenchmarkWrong
BenchmarkWrong-4        482026485                2.422 ns/op
BenchmarkWrong-4        495307399                2.413 ns/op
BenchmarkWrong-4        407674614                2.613 ns/op
PASS
ok      example.com/benchmark   4.439s

通过下面的输入后果,同样能够看出编译器没有做内联优化了,最终的执行效率和第一种计划基本一致。

测试源代码地址:benchmark 性能测试源代码,大家能够下载到本地进行测试。

备注: 网上有些文章的说法是把函数调用的后果赋值给一个局部变量,而后应用一个全局变量来承接这个局部变量的值就能够防止编译器的内联优化。这个说法实际上是谬误的,原作者 Teiva Harsanyi 在这方面也犯了谬误。要判断编译器是否做了内联优化,参考本文写的形式验证即可。

开源地址

文章和示例代码开源在 GitHub: Go 语言高级、中级和高级教程。

公众号:coding 进阶。关注公众号能够获取最新 Go 面试题和技术栈。

集体网站:Jincheng’s Blog。

知乎:无忌。

References

  • https://itnext.io/the-top-10-…
  • https://codeantenna.com/a/xxY…
  • gcflag 参数阐明:https://pkg.go.dev/cmd/compile
  • https://dave.cheney.net/2018/…
退出移动版