我们上一次讲到的是单元测试的有关内容,今天呢,我们再来讲一下基准测试benchmarking
的相关内容。
基准测试是一种测量和评估软件性能指标的活动。
在_test.go
为结尾的测试文件中,基准测试函数的格式如下:
func BenchmarkXxx(*testing.B)
通过go test
命令,再加上-bench
选项来执行,多个基准测试按照顺序来执行。
函数样例如下:
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
基准测试函数会运行目标代码b.N
次,在基准执行期间,会不断调整b.N
直到基准测试函数持续足够长的时间。
在运行基准测试时,也要使用go test
命令,不过要在前面加上-bench=
的标记,它接受一个表达式作为参数,匹配基准测试的函数,.
表示运行所有基准测试。
除此之外,因为默认情况go test
会执行单元测试,为了防止单元测试的影响对基准测试的输出,我们选择使用-run=
匹配一个不存在的单元测试方法即可,一般,我们不会使用none
来作为单元测试的名称,因此,我们可以使用-run=none
,因此,在执行基准测试时的命令如下:
go test -bench=. -run=none
在执行benchmarking
的时间默认为1s
,但如果我们想要让测试运行的时间更长一些,我们可以使用-benchtime
来进行指定,如果是3s
的话,我们就可以写成:
go test -bench=. -benchtime=3s -run=none
如果我们在运行前基准测试需要一些耗时的配置,那么我们可以先重置定时器:
func BenchmarkBigLen(b *testing.B) {
...... // 初始化代码
b.ResetTimer()
...... // 通常是一个for循环
}
如果基准测试要在并行设置中测试性能,那么我们可以使用RunParallel
辅助函数:
func BenchmarkTemplateParallel(b *testing.B) {
templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
b.RunParallel(func (pb *testing.PB) {
// every goroutine has a bytes.Buffer
var buf bytes.Buffer
for pb.Next() {
// all the goroutine run b.N times
buf.Reset()
templ.Execute(&buf, "World")
}
})
}
性能对比
前面的这个例子是一个int
类型转换为string
类型的例子,但标准库里面其实还有几种方法,我们来看一下:
package testing2
import (
"fmt"
"strconv"
"testing"
)
func BenchmarkSprintf(b *testing.B) {
num := 10
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", num)
}
}
func BenchmarkFormat(b *testing.B) {
num := int64(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.FormatInt(num, 10)
}
}
func BenchmarkItoa(b *testing.B) {
num := 10
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.Itoa(num)
}
}
/** The result is:
go test -bench=. -run=none
goos: linux
goarch: amd64
BenchmarkSprintf-4 20000000 90.8 ns/op
BenchmarkFormat-4 500000000 3.34 ns/op
BenchmarkItoa-4 300000000 5.33 ns/op
PASS
ok _/home/hdc/goProgramming/testing2 6.064s
*/
我们可以通过结果很直观的看出来谁比较快,谁的性能更好一些。
为了进一步分析三个函数快慢的根源,可以通过-benchmem
来分析内存的使用情况。-benchmem
可以提供每次操作分配内存的次数,以及每次操作分配的字节数。
$ go test -bench=. -benchmem -run=none
goos: linux
goarch: amd64
BenchmarkSprintf-4 20000000 91.9 ns/op 16 B/op 2 allocs/op
BenchmarkFormat-4 500000000 3.37 ns/op 0 B/op 0 allocs/op
BenchmarkItoa-4 300000000 5.30 ns/op 0 B/op 0 allocs/op
PASS
ok _/home/hdc/goProgramming/testing2 6.084s
我们发现结果中多出了两个参数,一个参数的单位是B/op
,表示的是每次操作分配的B
数,另一个参数的单位是allocs/op
,表示的是每次操作从堆上分配内存的次数。
pprof
除了前面所说的几个可以用来查看的参数选项,还可以在对程序运行进行分析的时候使用下面几个选项参数:
-bench regexp
:regexp
可以是任何正则表达式,表示需要运行的基准测试函数,一般可以使用-bench.
来执行当前目录下所有的基准测试;
-benchmem
:在输出内容中包含基准测试的内存分配统计信息;
-benchtime t
:t
表示执行单个测试函数的累计耗时上限,默认是1s
;
-cpuprofile out
:输出cpu profile
到指定的路径,可以使用pprof
来查看;
-memprofile out
:输出内存profile
到指定路径,可以使用pprof
来查看
我们在执行基准测试时,可以指定相关的选项,比如像下面这样:
go test -bench . --benchmem -cpuprofile cpu.prof
执行后查看目录,多了一个cpu.prof
的和一个testing2.test
的文件,但这个文件是无法直接查看的,需要使用工具去解析,这是我们就可以使用pprof
了。
这是一个Go语言提供的性能分析工具,可以分析cpu profile
、memory profile
、heap forfile
、block profile
等信息。
go tool pprof testing2.test cpu.prof
执行上述命令即可。
$ go tool pprof testing2.test cpu.prof
File: testing2.test
Type: cpu
Time: May 16, 2019 at 1:46pm (CST)
Duration: 6.11s, Total samples = 6.04s (98.93%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 5110ms, 84.60% of 6040ms total
Dropped 31 nodes (cum <= 30.20ms)
Showing top 10 nodes out of 27
flat flat% sum% cum cum%
2480ms 41.06% 41.06% 2480ms 41.06% strconv.FormatInt
750ms 12.42% 53.48% 1820ms 30.13% strconv.Itoa
590ms 9.77% 63.25% 2000ms 33.11% _/home/hdc/goProgramming/testing2.BenchmarkFormat
340ms 5.63% 68.87% 400ms 6.62% runtime.mallocgc
240ms 3.97% 72.85% 2060ms 34.11% _/home/hdc/goProgramming/testing2.BenchmarkItoa
190ms 3.15% 75.99% 450ms 7.45% fmt.newPrinter
140ms 2.32% 78.31% 300ms 4.97% fmt.(*fmt).fmt_integer
130ms 2.15% 80.46% 540ms 8.94% fmt.(*pp).doPrintf
130ms 2.15% 82.62% 260ms 4.30% sync.(*Pool).Get
120ms 1.99% 84.60% 220ms 3.64% sync.(*Pool).Put
(pprof)
此时我们就可以查看测试过程中最耗CPU
的资源了。
我们来详细的看一下每一列的含义:
第一列表示的是取样点落在该函数里的总数,比如
2480ms
就代表的是248
次(因为每一次固定时间为10ms
);
第二列表示的是落在该函数里取样点占总取样点的百分比;
第三列表示的是前几行加起来的执行时间占总共执行时间的多少;
第四列表示取样点落在该函数里和它直接调用、间接调用的函数里的总数;
第五列表示的是第四列的时间占总时间的百分比。
我们在查看耗费CPU
资源时可以使用命令行来控制,也可以通过使用可视化图形界面进行查看(安装Graphviz
),更进一步,我们还可以通过list
命令查看具体哪一步在耗时。
(pprof) list BenchmarkItoa
Total: 6.04s
ROUTINE ======================== _/home/hdc/goProgramming/testing2.BenchmarkItoa in /home/hdc/goProgramming/testing2/request_test.go
240ms 2.06s (flat, cum) 34.11% of Total
. . 23:}
. . 24:
. . 25:func BenchmarkItoa(b *testing.B) {
. . 26: num := 10
. . 27: b.ResetTimer()
100ms 100ms 28: for i := 0; i < b.N; i++ {
140ms 1.96s 29: strconv.Itoa(num)
. . 30: }
. . 31:}
. . 32:/** The result is:
. . 33:go test -bench=. -run=none
. . 34:goos: linux
(pprof)
其中第一列表示该行的执行时间,第二列表示该行的总执行时间。
这样,我们就可以知道函数的整体执行情况了。