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来进行性能基准数据的比较。