Go benchmark 详解

前言
基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器。
在日常生活中,我们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理,我们可以使用”单位时间内程序运行的次数“来衡量程序的性能。
在日常开发中,如果和同事在代码实现上有分歧,不用多费口舌,跑个分就知道谁牛X。

注意:在进行基准测试时,硬件资源直接影响测试结果,为了保证测试结果的可重复性,需要尽可能地保证硬件资源一致。(单一变量原则)
快速开始
创建项目 learnGolang
mkdir learnGolang
cd learnGolang
go mod init learnGolang
创建文件 main.go,编写我们的被测函数
package main
// 斐波那契数列
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func sum(a, b int) int {
return a + b
}
创建文件 main_test.go ,编写基准测试用例
package main
import "testing"
func BenchmarkFib10(b *testing.B) {
for n := 0; n < b.N; n++ {
fib(10)
}
}
func BenchmarkFib20(b *testing.B) {
for n := 0; n < b.N; n++ {
fib(20)
}
}
func BenchmarkSum(b *testing.B) {
for n := 0; n < b.N; n++ {
sum(1, 2)
}
}
- 位于同一个
package内的测试文件以_test.go结尾,其中的测试用例格式为func BenchmarkXxx(b *testing.B),注意Xxx首字母要大写(即驼峰命名法) - 函数内被测函数循环执行 b.N 次
开始运行
$ go test -bench=. .
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkFib10-4 3360627 362 ns/op
BenchmarkFib20-4 26676 44453 ns/op
BenchmarkSum-4 1000000000 0.296 ns/op
PASS
ok learnGolang 3.777s
go test [packages]指定测试范围
| 方法一 | 方法二 | |
|---|---|---|
| 运行当前 package 内的用例 | go test packageName | go test . |
| 运行子 package 内的用例 | go test packageName/subName | go test ./subName |
| 递归运行所有的用例 | go test packageName/... | go test ./... |
go test命令默认不执行 benchmark 测试,需要加上-bench参数,该参数支持正则表达式,只有匹配到的测试用例才会执行,使用.则运行所有测试用例
# 只运行斐波那契数列测试用例
$ go test -bench='.*Fib.*' .
goos: windows
goarch: amd64
pkg: learnGolang
BenchmarkFib10-4 3287449 357 ns/op
BenchmarkFib20-4 27097 44461 ns/op
PASS
ok learnGolang 3.418s
- BenchmarkFib10-4 中的 4 即
GOMAXPROCS,默认等于 CPU 核数

3287449 357 ns/op表示单位时间内(默认是1s)被测函数运行了 3287449 次,每次运行耗时 357ns,3287449*357ns=1.173s(耗时比 1s 略多,因为测试用例执行、销毁等是需要时间的)
ok learnGolang 3.418s表示本次测试总耗时
进阶参数
-benchtime t
在高中物理学中,由于测试物体瞬时速度不好实现,我们可以让物体多移动一段时间,然后采用“总距离/时间段”算出平均速度来代替瞬时速度。
go benchmark 默认测试时间是 1s,同样的原理,为了提升测试准确度,我们可以使用该参数适当增加时长。
➜ learnGolang go test -bench='Fib10$'
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 4153650 288 ns/op
PASS
ok learnGolang 1.491s
# 指定时长为 5s
➜ learnGolang go test -bench='Fib10$' -benchtime=5s
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 20616992 288 ns/op
PASS
ok learnGolang 6.235s
还是高中物理学,我们也可以指定物理移动的距离,然后测量所耗费的时间,计算平均速度。
该参数还支持特殊的形式 Nx ,用来指定被测程序的运行次数。
# 指定运行次数为 1000 次
➜ learnGolang go test -bench='Fib10$' -benchtime=1000x
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 1000 305 ns/op
PASS
ok learnGolang 0.002s
-count n
同样类似与测量物体速度,为了提升精确度,我们多做几次测试。
➜ learnGolang go test -bench='Fib10$' -benchtime=5s -count=3
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkFib10-12 19596388 288 ns/op
BenchmarkFib10-12 20796957 290 ns/op
BenchmarkFib10-12 20492478 291 ns/op
PASS
ok learnGolang 18.542s
-cpu n
该参数可以设置 benchmark 所使用的 CPU 核数。
下面我们模拟一次多核并行计算的例子,并观察设置不同核数后的测试结果
// main.go
func parallelExam() int {
chs := make([]chan int, 10) // 设置 10 个协程去并行计算
for i := 0; i < len(chs); i++ {
chs[i] = make(chan int, 1)
go parallelSum(chs[i])
}
sum := 0
for _, ch := range chs {
res := <-ch
sum += res
}
return sum
}
func parallelSum(ch chan int) {
defer close(ch)
sum := 0
for i := 1; i <= 100000; i++ { // 10万
sum += i
}
ch <- sum
}
// main_test.go
func BenchmarkParallelExam(b *testing.B) {
for n := 0; n < b.N; n++ {
parallelExam()
}
}
➜ learnGolang go test -bench='BenchmarkParallelExam' -cpu=1,4,6,10,12
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkParallelExam 3154 366754 ns/op
BenchmarkParallelExam-4 9316 119747 ns/op
BenchmarkParallelExam-6 10000 107040 ns/op
BenchmarkParallelExam-10 10000 108144 ns/op
BenchmarkParallelExam-12 9891 110018 ns/op
PASS
ok learnGolang 5.604s
从运行结果看出,随着 CPU 核数的增加,性能逐步提升,但是到一定阈值后,性能趋于稳定,此时再增加 CPU 核数,性能反而下降,因为 CPU 核心之间的切换也是需要成本的。
-benchmem
除了速度,内存分配情况也是需要我们重点关注的指标。
go 语言中,slice 有一个 cap 属性,合理的设置该值,可以减少内存分配次数,分配大小,提升程序性能。
// main.go
func sliceNoCap() {
s := make([]int, 0) // 未设置 cap 值
for i := 0; i < 10000; i++ {
s = append(s, i)
}
}
func sliceWithCap() {
s := make([]int, 0, 10000) // 预先设置 cap 值
for i := 0; i < 10000; i++ {
s = append(s, i)
}
}
// main_test.go
func BenchmarkSliceNoCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sliceNoCap()
}
}
func BenchmarkSliceWithCap(b *testing.B) {
for n := 0; n < b.N; n++ {
sliceWithCap()
}
}
➜ learnGolang go test -bench='Cap$' -benchmem .
goos: linux
goarch: amd64
pkg: learnGolang
BenchmarkSliceNoCap-12 31318 38614 ns/op 386297 B/op 20 allocs/op
BenchmarkSliceWithCap-12 111764 10269 ns/op 81920 B/op 1 allocs/op
PASS
ok learnGolang 2.858s
可以看到前者每次执行会分配 386297 字节的内存,约等于后者的 3.76 倍,每次执行会分配内存 20 次,是后者的 20 倍。
注意事项
ResetTimer
If a benchmark needs some expensive setup before running, the timer may be reset
如果在整个 benchmark 执行前,需要一些耗时的准备工作,我们需要将这部分耗时忽略掉
func BenchmarkFib(b *testing.B) {
time.Sleep(3 * time.Second) // 模拟耗时的准备工作
b.ResetTimer() // 重置计时器,忽略前面的准备时间
for n := 0; n < b.N; n++ {
fib(10)
}
}
StopTimer & StartTimer
StopTimer stops timing a test. This can be used to pause the timer while performing complex initialization that you don't want to measure.
StartTimer starts timing a test. This function is called automatically before a benchmark starts, but it can also be used to resume timing after a call to StopTimer.
如果在被测函数每次执行前,需要一些准备工作,我们可以使用 StopTimer 暂停计时,准备工作完成后,使用 StartTimer 继续计时。
func BenchmarkFib(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer() // 暂停计时
prepare() // 每次函数执行前的准备工作
b.StartTimer() // 继续计时
funcUnderTest() // 被测函数
}
}
参考
Go benchmark 详解的更多相关文章
- 负载均衡之Haproxy配置详解(及httpd配置)
下图描述了使用keepalived+Haproxy主从配置来达到能够针对前段流量进行负载均衡到多台后端web1.web2.web3.img1.img2.但是由于haproxy会存在单点故障问题,因此使 ...
- Protocol Buffers编码详解,例子,图解
Protocol Buffers编码详解,例子,图解 本文不是让你掌握protobuf的使用,而是以超级细致的例子的方式分析protobuf的编码设计.通过此文你可以了解protobuf的数据压缩能力 ...
- iOS开发——UI篇OC篇&SpriteKit详解
SpriteKit详解 SpriteKit,iOS/Mac游戏制作的新纪元 这是我的WWDC2013系列笔记中的一篇,完整的笔记列表请参看这篇总览.本文仅作为个人记录使用,也欢迎在许可协议范围内转载或 ...
- Linux下的I/O复用与epoll详解
前言 I/O多路复用有很多种实现.在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术.尽管 ...
- epoll机制详解
epoll机制详解 大牛的详解 epoll详解 什么是epoll? epoll是为处理大批量句柄而作了改进的poll, 是性能最好的多路I/O就绪通知方法; 只有三个系统调用: epoll_creat ...
- vue和react全面对比(详解)
vue和react对比(详解) 放两张图镇压小妖怪 本文先讲共同之处, 再分析区别 大纲在此: 共同点: a.都使用虚拟dom b.提供了响应式和组件化的视图组件 c.注意力集中保持在核心库,而将其他 ...
- (Dos)/BAT命令入门与高级技巧详解(转)
目录 第一章 批处理基础 第一节 常用批处理内部命令简介 1.REM 和 :: 2.ECHO 和 @ 3.PAUSE 4.ERRORLEVEL 5.TITLE 6.COLOR 7.mode 配置系统设 ...
- 守护客户数据价值:企业级NewSQL HTAP分布式云TBase架构详解
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 作者:jasonys,隶属于腾讯技术工程事业群数据平台部,负责TBase数据的技术研发和架构设计,有超过10年的数据库内核开发设计经验,完成 ...
- (转)Linux下select, poll和epoll IO模型的详解
Linux下select, poll和epoll IO模型的详解 原文:http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll ...
随机推荐
- 趁五一撸个纯Flutter版的心情日记App
前言 最近真的是太忙了,只能趁着五一期间把之前Android版的心情日记App移植到Flutter平台,并且已经上架华为应用市场以及苹果App Store. 整体框架沿用了两个星期,用Flutter撸 ...
- 源码篇:ThreadLocal的奇思妙想(万字图文)
前言 ThreadLocal的文章在网上也有不少,但是看了一些后,理解起来总感觉有绕,而且看了ThreadLocal的源码,无论是线程隔离.类环形数组.弱引用结构等等,实在是太有意思了!我必须也要让大 ...
- 17- web测试面试题
- Python脚本模拟登陆DVWA
目录 requests模拟登陆 Selenium自动化测试登陆 环境:python3.7 windows requests模拟登陆 我们登陆DVWA的时候,看似只有一步:访问网站,输入用户名和密码,登 ...
- JSON和JSONP的区别及使用方法
JSON(JavaScript Object Notation)和JSONP(JSON with Padding)虽然只有一个字母的差别,但其实他们根本不是一回事儿:JSON是一种数据交换格式,而JS ...
- 二、jmeter模拟请求头及监听器之结果树
一.模拟请求头 利用jmeter发送http请求时,被接收的服务端会对发送的该请求进行初步判断,如果不是web端发送的请求就会被打回导致请求不通,这时候需要模拟请求头,模拟正常的用户行为进行发送请求 ...
- 三、多线程之Thread与Runnable的区别
Thread与Runnable的区别(用三个窗口同时出售10张车票为例子) 运行代码 运行结果 分析 System.out.println("开始测试多线程");class MyT ...
- 腾讯暑期 前后七面 + hr(已拿offer面经)
以下是时间线 魔方 魔术师工作室 3.19 一面(120mins) c++ struct和union区别? 指针和引用的区别? 左值和右值? 字节对齐的作用? 什么情况下需要自定义new? mallo ...
- Asp.NetCore Web开发之创建项目
这一节,开始讲一下如何创建一个Asp.netCore Web项目,有两种常用的方式,一种是通过.NetCore SDK使用命令创建,另一种如果你使用的VisualStudio,可以直接根据引导创建. ...
- VS里的 代码片段(Code snippet)很有用,制作也很简单
工欲善其事必先利其器,而 Visual Studio 就是我们的开发利器. 上一篇文章,介绍了一个很棒的快捷键,如果你还没用过这个快捷键,看完之后应该会豁然开朗.如果你已经熟练的应用它,也会温故而知新 ...