在优化代码或者决定算法选用的时候,性能是很重要的一个指标,比如我最近在做需求的时候需要用哈希算法做签名。一开始想都没想就想用md5,然后IDE上蹦出几个大字:md5有已知的安全问题,建议换成其他算法。然后就考虑换SHA256。问题会不会换一个算法导致性能急剧下降呢?
这时Go语言内置的Benchmark功能就很方便的派上用场了。注意,基准测试受环境影响大,应尽量保证环境稳定,测试时尽量CPU别同时干其他耗性能的事情,别开节能模式。
Go基准测试是基于Go单元测试的。基准测试放在和单测一样的包下,依旧是xxxx_test.go这样的包下。
与单测函数不同的是,基准测试的函数以Benchmark来做开头,签名像这样:
func BenchmarkXxx(b *testing.B) { ... }
基准测试的基本原理是让用户实现一个循环来调用被测算法,外层的函数传入要循环的次数,通过不断的尝试,使得 总耗时/循环次数
趋于稳定后就可以认为这个值就是在这个环境下被测算法的耗时,当然也可以通过指定循环次数等使得提前结束。
而外层函数就是通过 b.N
把循环次数传递给我们的,所以在基准测试内应该要有个像这样的循环:
for n := 0; n < b.N; n++ {
// 被测算法
}
在开始基准测试前,可能有一些很耗时的准备工作,这个时候就可以通过b.ResetTimer()
重新开始计时
如果是在每次循环前后都需要做耗时的准备工作,则可以使用b.StartTimer()
和b.StopTimer()
的组合,来忽略那些准备阶段的时间。
testing.b
也提供正常的测试里提供的那些函数,如Fail
使得测试失败,Run
使得运行子测试,Helper
来标记辅助函数。
运行单测的 go test指令默认是会忽略基准测试的。
为了让go test能够运行基准测试,我们需要直接指定-bench标签,其参数是正则表达式。如果要运行当前包下所有的基准测试:
go test -bench=.
这样就会运行当前路径下所有的基准测试。
如果要运行所有名字包含go或lang的测试:
go test -bench="go|lang"
另外,由于默认会运行单测,为了不让单测的输出影响输出结果,可以故意指定个不存在的单测函数名的:
go test -bench="go|lang" -run=noExist
还有一些其他的相关指令:
option | 描述 |
---|---|
-benchmem | 性能测试的时候显示测试函数的内存分配的统计信息,等价于在基准测试中调用b.ReportAllocs() |
-count n | 运行多少次,默认1次 |
-timeout t | 超时时间,超过会panic,默认10分钟 |
-cpu | 指定GOMAXPROCS,可以通过,传入一个列表 |
-benchtime | 指定执行时间(e.g. 2s)或具体次数(e.g. 10x) |
继续我们的故事,我现在需要测测用md5和SHA256生成签名的效率差异,实际使用中是给一个uuid生成签名。于是基准测试就写成了这样:
util_test.go:
package util
import (
"crypto/md5"
"crypto/sha256"
"github.com/google/uuid"
"testing"
)
func BenchmarkSha256(b *testing.B) {
target := []byte(uuid.New().String())
b.ResetTimer()
for n := 0; n < b.N; n++ {
sha256.Sum256(target)
}
}
func BenchmarkMd5(b *testing.B) {
target := []byte(uuid.New().String())
b.ResetTimer()
for n := 0; n < b.N; n++ {
md5.Sum(target)
}
}
运行测试:
admin@……:util$ go test -bench=. -run=none
goos: darwin
goarch: amd64
pkg: ……/util
BenchmarkSha256-12 6331168 184 ns/op
BenchmarkMd5-12 11321952 103 ns/op
PASS
ok ……/util 4.392s
看到报告里头函数后面的-12了吗?这个表示运行时对应的 GOMAXPROCS 的值。接着的 6331168 表示最后一次给的N值,也就是认为结果可信的那次的循环的次数,最后的 184 ns/op表示每次循环需要花费 184 纳秒。
我们可以看出,SHA256在uuid字符串的场景下,计算费时差不多是Md5的1.8倍,还算可以接受。
可以多用上几个选项来看看
admin@……:util$ go test -bench=. -run=none -count=3 -cpu=2,4 -benchmem
goos: darwin
goarch: amd64
pkg: ……/util
BenchmarkSha256-2 6375644 178 ns/op 0 B/op 0 allocs/op
BenchmarkSha256-2 6575397 180 ns/op 0 B/op 0 allocs/op
BenchmarkSha256-2 6646250 182 ns/op 0 B/op 0 allocs/op
BenchmarkSha256-4 6566167 183 ns/op 0 B/op 0 allocs/op
BenchmarkSha256-4 6476132 190 ns/op 0 B/op 0 allocs/op
BenchmarkSha256-4 6327001 192 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-2 10067620 107 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-2 11456790 104 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-2 11314701 106 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-4 10312569 105 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-4 10565292 102 ns/op 0 B/op 0 allocs/op
BenchmarkMd5-4 11695822 103 ns/op 0 B/op 0 allocs/op
PASS
ok ……/util 17.036s
上面的测试中,我们指定了要运行3趟测试,分别用2核和4核来测试,并输出了内存的数据。可以看出,几次测试有波动,核数增加对运行速度并没什么软用,应为这两个算法都是串行调用。而且这两算法都不需要分配内存。
当你没法确定你的优化到底有没效果时,不懂到时候绩效咋写时,不妨试试用基准测试测下性能吧!