Go语言精进之路:从新手到高手的编程思想、方法和技巧(2)
上QQ阅读APP看书,第一时间看更新

46.3 使用性能基准比较工具

现在我们已经可以通过Go原生提供的性能基准测试为被测对象建立性能基准了。但被测代码更新前后的性能基准比较依然要靠人工计算和肉眼比对,十分不方便。为此,Go核心团队先后开发了两款性能基准比较工具:benchcmp(https://github.com/golang/tools/tree/master/cmd/benchcmp)和benchstat(https://github.com/golang/perf/tree/master/benchstat)。

1. benchcmp

benchcmp上手快,简单易用,对于输出的比较结果我们无须参考文档帮助即可自行解读。下面看一个使用benchcmp进行性能基准比较的例子。

// chapter8/sources/benchmark-compare/strcat_test.go

var sl = []string{
    "Rob Pike ",
    "Robert Griesemer ",
    "Ken Thompson ",
}

func Strcat(sl []string) string {
    return concatStringByOperator(sl)
}

func concatStringByOperator(sl []string) string {
    var s string
    for _, v := range sl {
        s += v
    }
    return s
}

func concatStringByJoin(sl []string) string {
    return strings.Join(sl, "")
}

func BenchmarkStrcat(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Strcat(sl)
    }
}

上面例子中的被测目标为Strcat。最初Strcat使用通过Go原生的操作符("+")连接的方式实现了字符串的连接。我们采集一下它的性能基准数据:

$go test -run=NONE -bench . strcat_test.go > old.txt

然后,升级Strcat的实现,采用strings.Join函数来实现多个字符串的连接:

func Strcat(sl []string) string {
    return concatStringByJoin(sl)
}

再采集优化后的性能基准数据:

$go test -run=NONE -bench . strcat_test.go > new.txt

接下来就轮到benchcmp登场了:

$benchcmp old.txt new.txt
benchmark             old ns/op     new ns/op     delta
BenchmarkStrcat-8     92.4          49.6          -46.32%

我们看到,benchcmp接受被测代码更新前后的两次性能基准测试结果文件——old.txt和new.txt,并将这两个文件中的相同基准测试(比如这里的BenchmarkStrcat)的输出结果进行比较。

如果使用-count对BenchmarkStrcat执行多次,那么benchcmp给出的结果如下:

$go test -run=NONE -count 5 -bench . strcat_test.go > old.txt
$go test -run=NONE -count 5 -bench . strcat_test.go > new.txt

$benchcmp old.txt new.txt
benchmark             old ns/op     new ns/op     delta
BenchmarkStrcat-8     92.8          51.4          -44.61%
BenchmarkStrcat-8     91.9          55.3          -39.83%
BenchmarkStrcat-8     96.1          52.6          -45.27%
BenchmarkStrcat-8     89.4          50.2          -43.85%
BenchmarkStrcat-8     91.2          51.5          -43.53%

如果向benchcmp传入-best命令行选项,benchcmp将分别从old.txt和new.txt中挑选性能最好的一条数据,然后进行比较:

$benchcmp -best old.txt new.txt
benchmark             old ns/op     new ns/op     delta
BenchmarkStrcat-8     89.4          50.2          -43.85%

benchcmp还可以按性能基准数据前后变化的大小对输出结果进行排序(通过-mag命令行选项):

$benchcmp -mag old.txt new.txt
benchmark             old ns/op     new ns/op     delta
BenchmarkStrcat-8     96.1          52.6          -45.27%
BenchmarkStrcat-8     92.8          51.4          -44.61%
BenchmarkStrcat-8     89.4          50.2          -43.85%
BenchmarkStrcat-8     91.2          51.5          -43.53%
BenchmarkStrcat-8     91.9          55.3          -39.83%

不过性能基准测试的输出结果受到很多因素的影响,比如:同一测试的运行次数;性能基准测试与其他正在运行的程序共享一台机器;运行测试的系统本身就在虚拟机上,与其他虚拟机共享硬件;现代机器的一些节能和功率缩放(比如CPU的自动降频和睿频)等。这些因素都会造成即便是对同一个基准测试进行多次运行,输出的结果也可能有较大偏差。但benchcmp工具并不关心这些结果数据在统计学层面是否有效,只对结果做简单比较。

2. benchstat

为了提高对性能基准数据比较的科学性,Go核心团队又开发了benchstat这款工具以替代benchcmp。下面用benchstat比较一下上面例子中的性能基准数据:

$benchstat old.txt new.txt
name      old time/op    new time/op   delta
Strcat-8  92.3ns ± 4%   52.2ns ± 6%   -43.43%  (p=0.008 n=5+5)

我们看到,即便old.txt和new.txt中各自有5次运行的数据,benchstat也不会像benchcmp那样输出5行比较结果,而是输出一行经过统计学方法处理后的比较结果。以第二列数据92.3ns ± 4%为例,这是benchcmp对old.txt中的数据进行处理后的结果,其中±4%是样本数据中最大值和最小值距样本平均值的最大偏差百分比。如果这个偏差百分比大于5%,则说明样本数据质量不佳,有些样本数据是不可信的。由此可以看出,这里new.txt中的样本数据是质量不佳的。

benchstat输出结果的最后一列(delta)为两次基准测试对比的变化量。我们看到,采用strings.Join方法连接字符串的平均耗时比采用原生操作符连接字符串短43%,这个指标后面括号中的p=0.008是一个用于衡量两个样本集合的均值是否有显著差异的指标。benchstat支持两种检验算法:一种是UTest(Mann Whitney UTest,曼-惠特尼U检验),UTest是默认的检验算法;另外一种是Welch T检验(TTest)。一般p值小于0.05的结果是可接受的。

上述两款工具都支持对内存分配数据情况的前后比较,这里以benchstat为例:

$go test -run=NONE -count 5 -bench . strcat_test.go -benchmem > old_with_mem.txt
$go test -run=NONE -count 5 -bench . strcat_test.go -benchmem > new_with_mem.txt

$benchstat old_with_mem.txt new_with_mem.txt
name      old time/op    new time/op    delta
Strcat-8    90.5ns ± 1%    50.6ns ± 2%  -44.14%  (p=0.008 n=5+5)

name      old alloc/op   new alloc/op   delta
Strcat-8     80.0B ± 0%     48.0B ± 0%  -40.00%  (p=0.008 n=5+5)

name      old allocs/op  new allocs/op  delta
Strcat-8      2.00 ± 0%      1.00 ± 0%  -50.00%  (p=0.008 n=5+5)

关于内存分配情况对比的输出独立于执行时间的输出,但结构上是一致的(输出列含义相同),这里就不再赘述了。

Go核心团队已经给benchcmp工具打上了“deprecation”(不建议使用)的标签,因此建议大家使用benchstat来进行性能基准数据的比较。