Go语言圣经
  • 前言
  • Go语言起源
  • Go语言项目
  • 本书的组织
  • 更多的信息
  • 致谢
  • 入门
    • Hello, World
    • 命令行参数
    • 查找重复的行
    • GIF动画
    • 获取URL
    • 并发获取多个URL
    • Web服务
    • 本章要点
  • 程序结构
    • 命名
    • 声明
    • 变量
    • 赋值
    • 类型
    • 包和文件
    • 作用域
  • 基础数据类型
    • 整型
    • 浮点数
    • 复数
    • 布尔型
    • 字符串
    • 常量
  • 复合数据类型
    • 数组
    • Slice
    • Map
    • 结构体
    • JSON
    • 文本和HTML模板
  • 函数
    • 函数声明
    • 递归
    • 多返回值
    • 错误
    • 函数值
    • 匿名函数
    • 可变参数
    • Deferred函数
    • Panic异常
    • Recover捕获异常
  • 方法
    • 方法声明
    • 基于指针对象的方法
    • 通过嵌入结构体来扩展类型
    • 方法值和方法表达式
    • 示例: Bit数组
    • 封装
  • 接口
    • 接口是合约
    • 接口类型
    • 实现接口的条件
    • flag.Value接口
    • 接口值
    • sort.Interface接口
    • http.Handler接口
    • error接口
    • 示例: 表达式求值
    • 类型断言
    • 基于类型断言识别错误类型
    • 通过类型断言查询接口
    • 类型分支
    • 示例: 基于标记的XML解码
    • 补充几点
  • Goroutines和Channels
    • Goroutines
    • 示例: 并发的Clock服务
    • 示例: 并发的Echo服务
    • Channels
    • 并发的循环
    • 示例: 并发的Web爬虫
    • 基于select的多路复用
    • 示例: 并发的字典遍历
    • 并发的退出
    • 示例: 聊天服务
  • 基于共享变量的并发
    • 竞争条件
    • sync.Mutex互斥锁
    • sync.RWMutex读写锁
    • 内存同步
    • sync.Once初始化
    • 竞争条件检测
    • 示例: 并发的非阻塞缓存
    • Goroutines和线程
  • 包和工具
    • 包简介
    • 导入路径
    • 包声明
    • 导入声明
    • 包的匿名导入
    • 包和命名
    • 工具
  • 测试
    • go test
    • 测试函数
    • 测试覆盖率
    • 基准测试
    • 剖析
    • 示例函数
  • 反射
    • 为何需要反射?
    • reflect.Type和reflect.Value
    • Display递归打印
    • 示例: 编码S表达式
    • 通过reflect.Value修改值
    • 示例: 解码S表达式
    • 获取结构体字段标识
    • 显示一个类型的方法集
    • 几点忠告
  • 底层编程
    • unsafe.Sizeof, Alignof 和 Offsetof
    • unsafe.Pointer
    • 示例: 深度相等判断
    • 通过cgo调用C代码
    • 几点忠告
  • 附录
    • 附录A:原文勘误
    • 附录B:作者译者
    • 附录C:译文授权
    • 附录D:其它语言
Powered by GitBook
On this page

Was this helpful?

  1. 测试

基准测试

基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数;*testing.B参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。

下面是IsPalindrome函数的基准测试,其中循环将执行N次。

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPalindrome("A man, a plan, a canal: Panama")
    }
}

我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但是这里总共只有一个基准测试函数,因此和-bench=IsPalindrome参数是等价的效果。

$ cd $GOPATH/src/gopl.io/ch11/word2
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000                1035 ns/op
ok      gopl.io/ch11/word2      2.179s

结果中基准测试名的数字后缀部分,这里是8,表示运行时对应的GOMAXPROCS的值,这对于一些和并发相关的基准测试是重要的信息。

报告显示每次调用IsPalindrome函数花费1.035微秒,是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间,它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间,然后推断一个较大的时间保证稳定的测量结果。

循环在基准测试函数内实现,而不是放在基准测试框架内实现,这样可以让每个基准测试函数有机会在循环启动前执行初始化代码,这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰,那么可以通过testing.B参数提供的方法来临时关闭或重置计时器,不过这些一般很少会用到。

现在我们有了一个基准测试和普通测试,我们可以很容易测试新的让程序运行更快的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查,这样可以避免每个比较都做两次:

n := len(letters)/2
for i := 0; i < n; i++ {
    if letters[i] != letters[len(letters)-1-i] {
        return false
    }
}
return true

不过很多情况下,一个明显的优化并不一定就能代码预期的效果。这个改进在基准测试中只带来了4%的性能提升。

$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000              992 ns/op
ok      gopl.io/ch11/word2      2.093s

另一个改进想法是在开始为每个字符预先分配一个足够大的数组,这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量,并指定合适的大小,像下面这样,

letters := make([]rune, 0, len(s))
for _, r := range s {
    if unicode.IsLetter(r) {
        letters = append(letters, unicode.ToLower(r))
    }
}

这个改进提升性能约35%,报告结果是基于2,000,000次迭代的平均运行时间统计。

$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 2000000                      697 ns/op
ok      gopl.io/ch11/word2      1.468s

如这个例子所示,快的程序往往是伴随着较少的内存分配。-benchmem命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况:

$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome    1000000   1026 ns/op    304 B/op  4 allocs/op

这是优化之后的结果:

$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome    2000000    807 ns/op    128 B/op  1 allocs/op

用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。

这个基准测试告诉我们所需的绝对时间依赖给定的具体操作,两个不同的操作所需时间的差异也是和不同环境相关的。例如,如果一个函数需要1ms处理1,000个元素,那么处理10000或1百万将需要多少时间呢?这样的比较揭示了渐近增长函数的运行时间。另一个例子:I/O缓存该设置为多大呢?基准测试可以帮助我们选择较小的缓存但能带来满意的性能。第三个例子:对于一个确定的工作那种算法更好?基准测试可以评估两种不同算法对于相同的输入在不同的场景和负载下的优缺点。

一般比较基准测试都是结构类似的代码。它们通常是采用一个参数的函数,从几个标志的基准测试函数入口调用,就像这样:

func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B)         { benchmark(b, 10) }
func Benchmark100(b *testing.B)        { benchmark(b, 100) }
func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }

通过函数参数来指定输入的大小,但是参数变量对于每个具体的基准测试都是固定的。要避免直接修改b.N来控制输入的大小。除非你将它作为一个固定大小的迭代计算输入,否则基准测试的结果将毫无意义。

基准测试对于编写代码是很有帮助的,但是即使工作完成了也应当保存基准测试代码。因为随着项目的发展,或者是输入的增加,或者是部署到新的操作系统或不同的处理器,我们可以再次用基准测试来帮助我们改进设计。

练习 11.6: 为2.6.2节的练习2.4和练习2.5的PopCount函数编写基准测试。看看基于表格算法在不同情况下对提升性能会有多大帮助。

练习 11.7: 为*IntSet(§6.5)的Add、UnionWith和其他方法编写基准测试,使用大量随机输入。你可以让这些方法跑多快?选择字的大小对于性能的影响如何?IntSet和基于内建map的实现相比有多快?

Previous测试覆盖率Next剖析

Last updated 4 years ago

Was this helpful?