天天看点

大厂必备技能:golang 性能调优 看我就够啦 - 基准测试

基准测试主要是通过测试CPU和内存的效率问题,来评估被测试代码的性能,并将该数据作为基准来比较每次调优后的性能是否有所改善,进而找到更好的解决方案。

(一)编写基准测试

首先我们先看一个基准测试的例子。是不是很像我们经常说的单元测试

func BenchmarkSprintf(b *testing.B){
	num:=10
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		fmt.Sprintf("%d",num)
	}
}
           
以下是go语言中基准测试需要关注的几个前提条件:
  1. 基准测试的代码文件必须以_test.go结尾
  2. 基准测试的函数必须以Benchmark开头
  3. 基准测试函数必须接受一个testing.B的指针作为唯一参数, 且基准测试函数不能有返回值
  4. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
  5. 如果每次循环迭代内部都有一些高成本的其他逻辑,请使用

    b.StopTimer()

    b.StartTimer()

    来暂停基准计时器,更关注于我们想要测试的函数
  6. 最后的for循环很重要,被测试的代码要放到循环里
  7. b.N是基准测试框架提供的,表示循环的次数,具体次数是按照机器性能在默认的1s的运行次数,因为需要反复调用测试的代码,才可以评估性能。
  8. 要显式地执行基准测试请使用

    -bench

    标识。

    -bench

    接收一个与待运行的基准测试名称相匹配的正则表达式,因此,如果要运行包中所有的基准测试,最常见的方法是这样写

    -bench=.

➜  go test -bench=. -run=none
BenchmarkSprintf-12     13088973                78.8 ns/op
PASS
ok      github.com/simonhgao/testsrv       1.536s
           

(二)基准测试的运行准备:

Testing flags

常用flag

  • -bench regexp

    :性能测试,支持表达式对测试函数进行筛选。-bench .则是对所有的benchmark函数测试
  • -benchmem

    :性能测试的时候显示测试函数的内存分配的统计信息
  • -cpu=n

    :指定GOMAXPROCS
  • -count n

    :运行测试和性能多少此,默认一次
  • -run regexp

    :只运行特定的测试函数, 比如-run ABC只测试函数名中包含ABC的测试函数
  • -timeout t

    :测试时间如果超过t, panic,默认10分钟
  • -v

    :显示测试的详细信息,也会把Log、Logf方法的日志显示出来
  • -cpuprofile=$FILE

    将 CPU 分析结果写入

    $FILE

    .
  • -memprofile=$FILE

    将内存分析结果写入

    $FILE

    ,

    -memprofilerate=N

    调整记录速率为

    1/N

    .
  • -blockprofile=$FILE

    , 将块分析结果写入

    $FILE

    .

因为默认情况下

go test

会运行单元测试,为了防止单元测试的输出影响我们查看基准测试的结果,可以使用

-run=

匹配单元测试方法,过滤掉单元测试的输出,我们这里使用

none

,因为我们不会创建这个名字的单元测试方法。

也可以使用

-run=^$

来过滤所有单元测试

go test -bench=. -run=^$
           

有些时候在benchmark之前需要做一些准备工作,并且,我们不希望这些准备工作纳入到计时里面,我们可以使用 b.ResetTimer(),代表重置计时为0,以调用时的时刻作为重新计时的开始。

看到上面运行测试结果

BenchmarkSprintf-12

后面的

-12

了吗?这个表示运行时对应的GOMAXPROCS的值,可以通过cpu的flag进行指定。

接着的

13088973

表示运行for循环的次数,也就是1s内调用被测试代码的次数

最后的

78.8 ns/op

表示每次需要话费78.8纳秒。(执行一次操作话费的时间)

如果想让测试运行的时间更长,可以通过-benchtime指定,比如3秒。

➜ go test -bench=. -run=none -benchtime=3s
BenchmarkSprintf-12     41707867                78.7 ns/op
PASS
ok      github.com/simonhgao/testsrv       3.735s
           

可以发现,我们加长了测试时间,测试的次数变多了,但是最终的性能结果:每次执行的时间,并没有太大变化。一般来说这个值最好不要超过3秒,意义不大。

性能对比(benchmem):

上面那个基准测试的例子,其实是一个int类型转为string类型的例子,标准库里还有几种方法,我们看下哪种性能更加.

func BenchmarkSprintf(b *testing.B){
	num:=10
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		fmt.Sprintf("%d",num)
	}
}

func BenchmarkFormat(b *testing.B){
	num:=int64(10)
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		strconv.FormatInt(num,10)
	}
}

func BenchmarkItoa(b *testing.B){
	num:=10
	b.ResetTimer()
	for i:=0;i<b.N;i++{
		strconv.Itoa(num)
	}
}
           
➜  go test -bench=. -benchmem -run=none
pkg: git.code.oa.com/simonhgao/testsrv
BenchmarkSprintf-12     13767098                78.1 ns/op            16 B/op          2 allocs/op
BenchmarkFormat-12      310182928                3.85 ns/op            0 B/op          0 allocs/op
BenchmarkItoa-12        284967086                4.15 ns/op            0 B/op          0 allocs/op
PASS
ok      github.com/simonhgao/testsrv       4.723s
           

-benchmem

可以提供每次操作分配内存的次数,以及每次操作分配的字节数。从结果我们可以看到,性能高的两个函数,每次操作都是进行1次内存分配,而最慢的那个要分配2次;性能高的每次操作分配0字节内存,而慢的那个函数每次需要分配16字节的内存。从这个数据我们就知道它为什么这么慢了,内存分配都占用都太高。

在代码开发中,对于我们要求性能的地方,编写基准测试非常重要,这有助于我们开发出性能更好的代码。不过性能、可用性、复用性等也要有一个相对的取舍,不能为了追求性能而过度优化。

(三)结合 pprof进行更形象化的分析:

pprof 性能监控

package bench
import "testing"
func Fib(n int) int {
    if n < 2 {
      return n
    }
    return Fib(n-1) + Fib(n-2)
}
func BenchmarkFib10(b *testing.B) {
    // run the Fib function b.N times
    for n := 0; n < b.N; n++ {
      Fib(10)
    }
}

           

可以同时看内存和CPU分析

go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
           

然后就可以用输出的文件使用pprof(进入pprof环境)

➜ go tool pprof profile.out
Type: cpu
Time: May 19, 2021 at 3:26pm (CST)
Duration: 1.56s, Total samples = 1.33s (85.34%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.33s, 100% of 1.33s total
Showing top 10 nodes out of 18
      flat  flat%   sum%        cum   cum%
     1.26s 94.74% 94.74%      1.27s 95.49%  github.com/simonhgao/testsrv.Fib
     0.05s  3.76% 98.50%      0.05s  3.76%  runtime.nanotime1
     0.01s  0.75% 99.25%      0.01s  0.75%  runtime.newstack
     0.01s  0.75%   100%      0.01s  0.75%  runtime.pthread_cond_signal
         0     0%   100%      1.27s 95.49%  github.com/simonhgao/testsrv.BenchmarkFib10
         0     0%   100%      0.01s  0.75%  runtime.findrunnable
         0     0%   100%      0.01s  0.75%  runtime.mcall
         0     0%   100%      0.05s  3.76%  runtime.mstart
         0     0%   100%      0.05s  3.76%  runtime.mstart1
         0     0%   100%      0.05s  3.76%  runtime.nanotime (inline)

           

然后你也可以用list命令检查函数需要的时间

(pprof) list Fib
     1.26s      1.98s (flat, cum) 148.87% of Total
         .          .      1:package main
         .          .      2:
         .          .      3:import "testing"
         .          .      4:
     440ms      450ms      5:func Fib(n int) int {
     150ms      150ms      6:   if n < 2 {
     110ms      110ms      7:           return n
         .          .      8:   }
     560ms      1.27s      9:   return Fib(n-1) + Fib(n-2)
         .          .     10:}
         .          .     11:func BenchmarkFib10(b *testing.B) {
         .          .     12:   // run the Fib function b.N times
         .          .     13:   for n := 0; n < b.N; n++ {
         .          .     14:           Fib(10)

           

图像分析

从 Go 1.11 开始, 火焰图等多种分析图被集成进入 Go 官方的 pprof 库.

# This will listen on :8081 and open a browser.
# Change :8081 to a port of your choice.
$ go tool pprof -http=":8081" [binary] [profile]
           

我们可以通过上述的方法来快速的生成一个本地网址,然后查看这些分析图

比如:

go tool pprof -http=":8081" profile.out

我们可以获得:

➜  go tool pprof -http=":8081" profile.out                       
Serving web UI on http://localhost:8081
           
大厂必备技能:golang 性能调优 看我就够啦 - 基准测试

如果遇到报错:Failed to execute dot. Is Graphviz installed? Error: exec: “dot”: executable file not found in %PATH% (第一次一定会碰到)

是你电脑没有安装graphviz导致的:

windows可以进入gvedit官网下载地址 下载稳定版

mac 安装比较简单, 执行下边的指令安装好后就可以使用web进行展现了

brew install graphviz
           

火焰图:

火焰图(Flame Graph)是 Bredan Gregg 创建的一种性能分析图表,因为它的样子近似火焰而得名。

火焰图 svg 文件可以通过浏览器打开,它对于调用图的最优点是它是动态的:可以通过点击每个方块来 zoom in 分析它上面的内容。

火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短。火焰图的配色并没有特殊的意义,默认的红、黄配色是为了更像火焰而已。

火焰图我们可以通过上边生成的网址http://localhost:8081下拉菜单选择 Flame Graph就可以了。

大厂必备技能:golang 性能调优 看我就够啦 - 基准测试

当然pprof还有更多的使用方法,详细的可看:tutorial

(四)总结:

  1. 进行性能测试时,尽可能保持测试环境的稳定
  2. 实现 benchmark 测试

    • 位于

    _test.go

    文件中

    • 函数名以

    Benchmark

    开头

    • 参数为

    b *testing.B

    b.ResetTimer()

    可重置定时器

    b.StopTimer()

    暂停计时

    b.StartTimer()

    开始计时
  3. 执行 benchmark 测试

    go test -bench .

    执行当前测试

    -bench

    可传入正则,匹配用例

    -cpu

    可改变 CPU 核数

    -benchtime

    可指定执行时间或具体次数

    -count

    可设置 benchmark 轮数

    -benchmem

    可查看内存分配量和分配次数
  4. pprof

    -memprofile -cpuprofile

    生成基准性能报告

    go tool pprof -http=":8081" [binary] [profile]

    生成本地图像分析

    使用pprof辅助优化我们的代码

Reference:

https://segmentfault.com/a/1190000016354758

https://segmentfault.com/a/1190000007951944

继续阅读