Go语言圣经 - 第11章 测试 - 11.4 - 11.6

第11章 测试

软件测试是一个巨大的领域,但是Go语言的测试技术是相对比较低级的,它依赖一个Go test测试命令和一组按照约定方式编写的测试函数,测试命令可以运行这些函数

在实践中,编写测试代码和编写程序本身并没有多大区别

11.4 基准测试

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

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

import "testing"

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

我们用下面的命令运行基准测试,和普通测试不同的是,默认情况下不会运行任何基准测试。需要用bench命令行标志参数指定,它是一个正则表达式,默认值是空的,可以匹配所有基准测试函数

$ 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的值,这对于一些与并发相关的基准测试是重要的信息

报告显示每调用一次函数花费的时间是1.035微秒,是执行1000000次的平均时间。因为基准测试驱动器开始并不知道每个测试函数运行所花的时间,它会先用较小的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%的分配调用次数和减少近一半的内存需求

上述基准测试告诉了我们某个具体操作的绝对时间,但是我们往往想知道两个不同操作的实践对比,如一个函数处理1000条数据需要1s,那么处理更多的需要多少时间呢?这样的比较揭示了渐进增长函数的运行时间。另一个例子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.5 剖析

基准测试能够衡量特定操作的性能,但是我们并不知道优化的时候该从何做起

其实我们在优化的时候首先要明确的一点是先把时间花费在完善程序的核心功能上,而不是着重提升那百分之几的优化

但是真的的到了优化环节,我们应该怎么着手呢

我们评估程序最好的办法就是性能剖析,剖析技术是基于程序执行期间的一些自动抽样,然后在收尾时进行推断;最后产生的统计结果就称为剖析数据

Go语言支持多种类型的剖析性能分析,每一种关注不同的方面,但它们都涉及到每个采样记录的感兴趣的一系列事件消息,每个事件都包含函数调用时函数调用堆栈信息,go test工具对几种方式都提供了支持

CPU剖析数据标识了最耗CPU时间的函数,CPU上运行的线程每隔几毫秒都会遇到操作系统的中断事件,每次中断时都会记录一个剖析数据然后回复正常运行

堆剖析则标识了最耗内存的语句,剖析库会记录调用内部内存分配的操作,平均每512KB的内存申请会触发一个剖析数据

阻塞剖析则记录阻塞goroutine最久的操作,例如系统调用,管道发送和接收,还有获取锁等,每当goroutine被这些操作阻塞时,剖析库都会记录相应的事件

只需要开启下面其中一个标志参数就可以生成各种分析文件。当同时使用多个标志参数时需要当心,因为一项分析操作可能会影响其它项的分析结果

$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out

对于一些非测试程序,也可剖析,具体实现方式:这个和程序大小关联比较大,尤其对于长程序非常有用,因此可以通过调用Go的runtime API来启用运行时剖析

一旦我们收集到了用于分析的采样数据,我们就可以使用pprof来分析这些数据。它是Go工具箱自带的一个工具,但并不是一个日常工具,它对应 go tool pprof命令。这个命令有很特性和选项,但是最基本的是两个参数:生成这个概要文件的可执行程序和对应的剖析数据

为了提高效率和减少空间,分析日志本身并不包含函数的名字:它只包含函数对应的地址。也就是说pprof需要对应的可执行程序来解读剖析数据

虽然go test通常在测试完成后就丢弃临时用的测试程序,但是在启用分析时,会将测试程序保存为一个foo.test文件其中foo部分对应待测包的名字

如下命令演示了如何收集并展示CPU分析文件,我们选择net/http包的一个基准测试为例,通常最好是对业务关键代码得部分设计专门得基准测试,因为简单的基准测试几乎没法代表业务场景,因此我们用-run=NONE参数禁止那些简单测试

$ go test -run=NONE -bench=ClientServerParallelTLS64 \
    -cpuprofile=cpu.log net/http
 PASS
 BenchmarkClientServerParallelTLS64-8  1000
    3141325 ns/op  143010 B/op  1747 allocs/op
ok       net/http       3.395s

$ go tool pprof -text -nodecount=10 ./http.test cpu.log
2570ms of 3590ms total (71.59%)
Dropped 129 nodes (cum <= 17.95ms)
Showing top 10 nodes out of 166 (cum >= 60ms)
    flat  flat%   sum%     cum   cum%
  1730ms 48.19% 48.19%  1750ms 48.75%  crypto/elliptic.p256ReduceDegree
   230ms  6.41% 54.60%   250ms  6.96%  crypto/elliptic.p256Diff
   120ms  3.34% 57.94%   120ms  3.34%  math/big.addMulVVW
   110ms  3.06% 61.00%   110ms  3.06%  syscall.Syscall
    90ms  2.51% 63.51%  1130ms 31.48%  crypto/elliptic.p256Square
    70ms  1.95% 65.46%   120ms  3.34%  runtime.scanobject
    60ms  1.67% 67.13%   830ms 23.12%  crypto/elliptic.p256Mul
    60ms  1.67% 68.80%   190ms  5.29%  math/big.nat.montgomery
    50ms  1.39% 70.19%    50ms  1.39%  crypto/elliptic.p256ReduceCarry
    50ms  1.39% 71.59%    60ms  1.67%  crypto/elliptic.p256Sum

参数 -text用于指定输出格式,在这里每行是一个函数,根据使用CPU的时间长短来排序。其中 -nodecount=10参数限制了只输出前10行的结果,对于严重的性能问题,这个文本格式基本可以帮助查明原因了

这个概要告诉我们,HTTPS基准测试crypto/elliptic.p256ReduceDegree函数占用了将近一半的CPU资源,对性能占很大比重,相比之下,如果一个概要文件中主要是runtime包的内存分配的函数,那么减少内存消耗可能是一个值得尝试的优化策略

对于一些更微妙的问题,我们可能得使用pprof的图形显示功能,这个需要安装GraphViz工具,,可以从 http://www.graphviz.org 下载。参数-web用于生成函数的有向图,标注有CPU的使用和最热点的函数等信息。

这一节我们只是简单看了下Go语言的数据分析工具。更多内容可以阅读Go官方博客的“Profiling Go Programs”一文

11.6 示例函数

第三种被go test特殊对待的函数是示例函数,以Example为函数名的开头,示例函数没有函数参数和返回值。如下是IsPalindrome函数对应的示例函数

func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
    fmt.Println(IsPalindrome("palindrome"))
    // Output:
    // true
    // false
}

示例函数有三个主要的用途,其中一个就是作为文档,因为例子往往比文字解释更加的直观

第二个用处:运行go test执行测试的时候也会运行示例函数,如果示例函数有//Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出和注释是否匹配

第三个用处是提供过一个真实的演练场,http://golang.org 就是由godoc提供的文档服务,它使用了Go Playground让用户可以在浏览器中在线编辑和运行每个示例函数。这通常是学习函数使用或Go语言特性最快捷的方式

最后两章是我们讨论的是reflect和unsafe包,一般的Go程序员很少使用它们,事实上我们在实际中也确实很少用到。所以基本的内容到这里就结束了

Go语言暂时完结撒花,反射等以后我们研究的更深入一点再回来补充,马上就过年了,时间刚刚好!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值