扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • 宝典简介

    • 《Go语言标准库》The Golang Standard Library by Example
  • 第一章 输入输出 (Input/Output)

  • 第二章 文本

  • 第三章 数据结构与算法

  • 第四章 日期与时间

  • 第五章 数学计算

  • 第六章 文件系统

  • 第七章 数据持久存储与交换

  • 第八章 数据压缩与归档

  • 第九章 测试

    • 第九章 测试
    • testing - 单元测试
    • testing - 基准测试
    • testing - 子测试与子基准测试
    • testing - 运行并验证示例
    • testing - 其他功能
    • httptest - HTTP 测试辅助工具
    • 总结
  • 第十章 进程、线程与 goroutine

  • 第十三章 应用构建 与 debug

  • 第十五章 底层库介绍

  • 第十六章 同步

扫码订阅《 》或入驻星球,即可阅读文章!

testing - 基准测试


polaris1119

# testing - 基准测试

在 _test.go 结尾的测试文件中,如下形式的函数:

func BenchmarkXxx(*testing.B)

被认为是基准测试,通过 go test 命令,加上 -bench 标志来执行。多个基准测试按照顺序运行。

基准测试函数的形式如下:

func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}
1
2
3
4
5

基准函数会运行目标代码 b.N 次。在基准执行期间,程序会自动调整 b.N 直到基准测试函数持续足够长的时间。输出结果形如:

BenchmarkHello    10000000    282 ns/op

意味着循环执行了 10000000 次,每次循环花费 282 纳秒 (ns)。

如果基准测试在循环前需要一些耗时的配置,则可以先重置定时器:

func BenchmarkBigLen(b *testing.B) {
    big := NewBig()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        big.Len()
    }
}
1
2
3
4
5
6
7

如果基准测试需要在并行设置中测试性能,则可以使用 RunParallel 辅助函数 ; 这样的基准测试一般与 go test -cpu 标志一起使用:

func BenchmarkTemplateParallel(b *testing.B) {
	templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
	b.RunParallel(func(pb *testing.PB) {
		// 每个 goroutine 有属于自己的 bytes.Buffer.
		var buf bytes.Buffer
		for pb.Next() {
			// 循环体在所有 goroutine 中总共执行 b.N 次
			buf.Reset()
			templ.Execute(&buf, "World")
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12

# 基准测试示例

接着上一节的例子,我们对 Fib 进行基准测试:

func BenchmarkFib10(b *testing.B) {
        for n := 0; n < b.N; n++ {
                Fib(10)
        }
}
1
2
3
4
5

执行 go test -bench=.,输出:

$ go test -bench=.
BenchmarkFib10-4   	 3000000	       424 ns/op
PASS
ok  	chapter09/testing	1.724s
1
2
3
4

这里测试了 Fib(10) 的情况,我们可能需要测试更多不同的情况,这时可以改写我们的测试代码:

func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }

func benchmarkFib(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		Fib(i)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

再次执行 go test -bench=.,输出:

$ go test -bench=.
BenchmarkFib1-4               	1000000000	         2.58 ns/op
BenchmarkFib2-4               	200000000	         7.38 ns/op
BenchmarkFib3-4               	100000000	        13.0 ns/op
BenchmarkFib10-4              	 3000000	       429 ns/op
BenchmarkFib20-4              	   30000	     54335 ns/op
BenchmarkFib40-4              	       2	 805759850 ns/op
PASS
ok  	chapter09/testing	15.361s
1
2
3
4
5
6
7
8
9

默认情况下,每个基准测试最少运行 1 秒。如果基准测试函数返回时,还不到 1 秒钟,b.N 的值会按照序列 1,2,5,10,20,50,... 增加,同时再次运行基准测测试函数。

我们注意到 BenchmarkFib40 一共才运行 2 次。为了更精确的结果,我们可以通过 -benchtime 标志指定运行时间,从而使它运行更多次。

$ go test -bench=Fib40 -benchtime=20s
BenchmarkFib40-4   	      30	 838675800 ns/op
1
2

# B 类型

B 是传递给基准测试函数的一种类型,它用于管理基准测试的计时行为,并指示应该迭代地运行测试多少次。

当基准测试函数返回时,或者当基准测试函数调用 FailNow、Fatal、Fatalf、SkipNow、Skip、Skipf 中的任意一个方法时,则宣告测试函数结束。至于其他报告方法,比如 Log 和 Error 的变种,则可以在其他 goroutine 中同时进行调用。

跟单元测试一样,基准测试会在执行的过程中积累日志,并在测试完毕时将日志转储到标准错误。但跟单元测试不一样的是,为了避免基准测试的结果受到日志打印操作的影响,基准测试总是会把日志打印出来。

B 类型中的报告方法使用方式和 T 类型是一样的,一般来说,基准测试中也不需要使用,毕竟主要是测性能。这里我们对 B 类型中其他的一些方法进行讲解。

# 计时方法

有三个方法用于计时:

  1. StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,我们也可以在调用 StopTimer 之后恢复计时;
  2. StopTimer:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;
  3. ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。本节开头有使用示例。

# 并行执行

通过 RunParallel 方法能够并行地执行给定的基准测试。RunParallel会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在 RunParallel 之前调用 SetParallelism(如 SetParallelism(2),则 goroutine 数量为 2*GOMAXPROCS)。RunParallel 通常会与 -cpu 标志一同使用。

body 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到 pb.Next 返回 false 值为止。因为 StartTimer、StopTime 和 ResetTimer 这三个方法都带有全局作用,所以 body 函数不应该调用这些方法; 除此之外,body 函数也不应该调用 Run 方法。

具体的使用示例,在本节开头已经提供!

# 内存统计

ReportAllocs 方法用于打开当前基准测试的内存统计功能, 与 go test 使用 -benchmem 标志类似,但 ReportAllocs 只影响那些调用了该函数的基准测试。

测试示例:

func BenchmarkTmplExucte(b *testing.B) {
	b.ReportAllocs()
	templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
	b.RunParallel(func(pb *testing.PB) {
		// Each goroutine has its own bytes.Buffer.
		var buf bytes.Buffer
		for pb.Next() {
			// The loop body is executed b.N times total across all goroutines.
			buf.Reset()
			templ.Execute(&buf, "World")
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13

测试结果类似这样:

BenchmarkTmplExucte-4   	 2000000	       898 ns/op	     368 B/op	       9 allocs/op

# 基准测试结果

对上述结果中的每一项,你是否都清楚是什么意思呢?

  • 2000000 :基准测试的迭代总次数 b.N
  • 898 ns/op:平均每次迭代所消耗的纳秒数
  • 368 B/op:平均每次迭代内存所分配的字节数
  • 9 allocs/op:平均每次迭代的内存分配次数

testing 包中的 BenchmarkResult 类型能为你提供帮助,它保存了基准测试的结果,定义如下:

type BenchmarkResult struct {
    N         int           // The number of iterations. 基准测试的迭代总次数,即 b.N
    T         time.Duration // The total time taken. 基准测试的总耗时
    Bytes     int64         // Bytes processed in one iteration. 一次迭代处理的字节数,通过 b.SetBytes 设置
    MemAllocs uint64        // The total number of memory allocations. 内存分配的总次数
    MemBytes  uint64        // The total number of bytes allocated. 内存分配的总字节数
}
1
2
3
4
5
6
7

该类型还提供了每次迭代操作所消耗资源的计算方法,示例如下:

package main

import (
	"bytes"
	"fmt"
	"testing"
	"text/template"
)

func main() {
	benchmarkResult := testing.Benchmark(func(b *testing.B) {
		templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
		// RunParallel will create GOMAXPROCS goroutines
		// and distribute work among them.
		b.RunParallel(func(pb *testing.PB) {
			// Each goroutine has its own bytes.Buffer.
			var buf bytes.Buffer
			for pb.Next() {
				// The loop body is executed b.N times total across all goroutines.
				buf.Reset()
				templ.Execute(&buf, "World")
			}
		})
	})

	// fmt.Printf("%8d\t%10d ns/op\t%10d B/op\t%10d allocs/op\n", benchmarkResult.N, benchmarkResult.NsPerOp(), benchmarkResult.AllocedBytesPerOp(), benchmarkResult.AllocsPerOp())
	fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 基准测试示例
  • B 类型
  • 计时方法
  • 并行执行
  • 内存统计
  • 基准测试结果