go语言中的基准测试

二维码
| Aug 18, 2020 | 原创

上一节介绍了如何在 Go 语言中进行单元测试,本节我们将介绍如何进行基准测试 (benchmark)。

基准测试,还是基于 _test.go 文件进行的,只要创建以 Benchmark 开头的测试函数即可,如我们在《使用Bcrypt保存密码》小节做的基准测试示例:

func BenchmarkMd5(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
    	_ = crypto.MD5("12345678")
    }
}

func BenchmarkBcrypt(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
    	_, _ = bcrypt.GenerateFromPassword([]byte("12345678"), 10)
    }
}

基准测试能够帮我们评估代码的性能如何,在这里我们编写了两个基准测试函数, BenchmarkMd5BenchmarkBcrypt 。主要是希望对比这俩两种数据加密 hash 方式,代码执行所需要消耗的时间。

最终运行的结果是 bcrypt 所消耗的平均时间是 md5222056 倍。正式因为 bcrypt 这种慢加密方式提高了密码破解的代价,从而提高了密码的安全性。具体内容,请回顾:《使用Bcrypt保存密码》小节内容。

字符串拼接性能测试

不过我们从这一点也可以看出,基准测试对于我们评估代码的性能是非常重要的,再比如:任何语言对于字符串拼接都提供多种方式, Go 语言同样如此:

  1. 第一种方式:使用 + 符号拼接
  2. 使用 strings.Builder
  3. 使用 fmt.Sprintf() 函数

我们暂且先列举这三种方式,那么,这三种方式到底哪一种方式的速度最快呢?这时候,基准测试便可以告诉你答案:

我们还是在 crypto 目录下新建一个: concat_test.go 文件,并编写如下三个基准测试函数:

// 拼接方式
func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
    	rel := ""
    	rel += strconv.Itoa(2020)
    	rel += "年, 祝大家"
    	rel += "万事如意"
    }
}

// Builder 方式
func BenchmarkBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
    	var builder strings.Builder
    	builder.WriteString(strconv.Itoa(2020))
    	builder.WriteString("年, 祝大家")
    	builder.WriteString("万事如意")
  
    	_ = builder.String()
    }
}

// fmt.Sprintf 
func BenchmarkPrint(b *testing.B) {
    for i := 0; i < b.N; i++ {
    	_ = fmt.Sprintf("%d年, 祝大家:%s", 2020, "万事如意")
    }
}

基准测试函数有如下特点:

  1. 基准测试的代码文件必须以 _test.go 结尾。
  2. 基准测试的函数必须以 Benchmark 开头。
  3. 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数。
  4. 基准测试函数不能有返回值。
  5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰。
  6. 被测试的代码需要放到 for 循环里。
  7. b.N 是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。但 b.N 并不是固定次数的。

如何运行

基准测试运行还是基于 go test 命令,只是需要添加 -bench 参数:

# 运行所有benchmarks
go test -bench .

但是 go test 命令默认还会运行单元测试的函数,因此还需要添加 -run ^$ 参数,意思是排除所有单元测试的执行,避免影响基准测试结果:

go test -bench . -run ^$

^$ 正则相当于匹配为空的的单元测试函数,因为单元测试函数都有名称,所以用这种方式过滤掉所有单元测试函数执行,当然你也可以写一个不存在的函数,例如:

go test -bench . -run none

运行指定基准测试函数

-bench regex 会根据之后输入参数匹配到的基准函数进行执行,因此和 -run 参数类型,可以利用此参数运行单个测试函数:

# 只运行 BenchmarkConcat 函数
go test -bench "BenchmarkConcat" -run ^$ 

# 运行 BenchmarkConcat 和 BenchmarkBuilder
go test -bench "^BenchmarkConcat|BenchmarkBuilder$" -run ^$

同样的,如果你是的用 IDE 环境,测试也不需要手敲代码,直接选择文件或者相关函数,使用编辑器的上下文菜单运行即可, IDE 会让工作变得更简单:

测试结果:

对于字符串的基准测试结果如下:

BenchmarkConcat-4    	 7296738	       141 ns/op
BenchmarkBuilder-4   	11617630	        91.4 ns/op
BenchmarkPrint-4     	 6809126	       163 ns/op

使用内置的 string.Builder 方法只需要 91.4 纳秒,而使用另外两种方法则需要消耗 1.5 倍左右的时间。由此可见,如果希望高效的拼接字符串,使用内置 strings.Builder 方法更高效一些,当然另外两种方法也不算太慢。

不过这里,抛出一个疑问给读者,我们在使用 + 方式做字符串拼接使用的是多次赋值拼接,如果一次性全部拼接效果又如何呢?即:

func BenchmarkConcatInline(b *testing.B) {
    for i := 0; i < b.N; i++ {
    	_ = strconv.Itoa(2020) + "年, 祝大家" +  "新年快乐"
    }
}

你不妨自己运行试试,如上结果所需要消耗的时间是多少?

总结

本节介绍了 Go 语言里使用基准测试帮我们测试代码性能问题,并且测试了不同的字符串拼接方式程序所花费的时间。从下一节开始,我将给大家分享关于 Go 语言代码部署上线相关内容。