go 如何进行 Benchmark 基准测试
目录
一、为什么要进行基准测试?
开发人员在开发过程中,对一些函数、方法进行性能优化,那么如何知道优化的结论呢?到底优化成功没有?优化了哪些呢?
比如内存少了,耗时少、cpu 占用降低了等等…
本文以【字符串高效拼接】为例,带你入门 go 的Benchmark 基准测试
二、Benchmark 基准测试
(一)准备
性能测试受环境的影响很大,为了保证测试的可重复性,在进行性能测试时,尽可能地保持测试环境的稳定。
- 机器处于闲置状态,测试时不要执行其他任务,也不要和其他人共享硬件资源。
- 机器是否关闭了节能模式,一般笔记本会默认打开这个模式,测试时关闭。
- 避免使用虚拟机和云主机进行测试,一般情况下,为了尽可能地提高资源的利用率,虚拟机和云主机 CPU 和内存一般会超分配,超分机器的性能表现会非常地不稳定。
⚠️:如果你确实没办法保证以上条件,那么只是在比对测试的时候,两次运行时机器环境要尽可能保持一致,确保基准测试的准确性
接下来以【字符串高效拼接:对字符串连续拼接20次】为例,进行基准测试,测试参数设置:
-
-cpu=2,4 : 用2,4核cpu分别进行测试
-
-count=3: 进行 3 轮测试
-
-benchmem:输出内存、cpu测试结论
(二)基准测试
拼接字符串 + 和 strings.Builder 性能对比
基准测试格式:
func Benchmark[测试函数名](b *testing.B){
for n := 0; n < b.N; n++ {
// 调用测试的函数
}
}
1. 拼接字符串 +
package main
import (
"testing"
)
func BenchmarkStringAdd(b *testing.B) {
for n := 0; n < b.N; n++ {
StringAdd()
}
}
func StringAdd() string {
var str = "生活是一面镜子。你对它笑 它就对你笑 你对它哭 它也对你哭。"
for i := 0; i < 100; i++ {
str += str
}
return str
}
运行:
mac@fiveyoboy learn-go % go test -bench='BenchmarkStringAdd' -cpu=2,4 -count=3 -benchmem .
goos: darwin // 操作系统
goarch: arm64 // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu
// 测试函数-使用CPU核数 执行次数 每次耗时ns(左移9位为秒) 分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringAdd-2 118 10313506 ns/op 176171351 B/op 20 allocs/op
BenchmarkStringAdd-2 99 10152684 ns/op 176171362 B/op 20 allocs/op
BenchmarkStringAdd-2 100 10406031 ns/op 176171354 B/op 20 allocs/op
// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringAdd-4 120 10045046 ns/op 176171401 B/op 20 allocs/op
BenchmarkStringAdd-4 100 10109044 ns/op 176171398 B/op 20 allocs/op
BenchmarkStringAdd-4 120 9977272 ns/op 176171405 B/op 20 allocs/op
PASS
ok fiveyoboy/learn-go 20.302s
完整说明请看之后的内容
2.拼接字符串 strings.Builder
package main
import (
"testing"
)
func BenchmarkStringBuilderAdd(b *testing.B) {
for n := 0; n < b.N; n++ {
StringBuilderAdd()
}
}
func StringBuilderAdd() string {
var builder strings.Builder
var str = "生活是一面镜子。你对它笑 它就对你笑 你对它哭 它也对你哭。"
builder.WriteString(str)
for i := 0; i < 20; i++ {
builder.WriteString(str)
}
return builder.String()
}
运行:
mac@fiveyoboy learn-go % go test -bench='BenchmarkStringBuilderAdd' -cpu=2,4 -count=3 -benchmem .
goos: darwin // 操作系统
goarch: arm64 // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu
// 测试函数-使用CPU核数 执行次数 每次耗时ns(左移9位为秒) 分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringBuilderAdd-2 2170213 546.5 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-2 2217442 548.2 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-2 2229522 543.1 ns/op 4320 B/op 6 allocs/op
// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringBuilderAdd-4 2068552 563.3 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-4 2069716 561.8 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-4 2103303 587.5 ns/op 4320 B/op 6 allocs/op
PASS
ok fiveyoboy/learn-go 9.302s
3.结论
对比两种基准测试结果,我相信你也能看出来结论了,无论是从内存分配、执行耗时来看,相同环境下,string.Builder 的性能要远远高于 “+”
三、执行参数说明
// 执行基准测试,-cpu 指定 cpu 核数,-count 执行轮数,-benchmem 输出cpu、内存测试结论, 点 . 表示在执行测试的目录为当前目录
go test -bench='BenchmarkStringBuilderAdd' -cpu=2,4 -count=3 -benchmem .
执行参数 | 简介 | 备注 |
---|---|---|
-bench=‘Fib$’ | 可传入正则,匹配用例 | 如只运行以 Fib 结尾的 benchmark 用例 |
-cpu=2,4 | 可改变 CPU 核数 | GOMAXPROCS,CPU核数,默认机器的核数 |
-benchtime=5s -benchtime=50x |
可指定执行时间或具体次数 | benchmark 的默认时间是 1s,决定了b.N的次数,测试时间 -benchtime的值除了是时间外,还可以是具体的次数。 例如,执行 30 次可以用-benchtime=30x |
-count=3 | 可设置 benchmark 轮数 | 参数可以用来设置 benchmark 的轮数,默认是1轮 |
-benchmem | 可查看内存分配量和分配次数 | 参数可以度量内存分配的次数,添加此参数后会在结果之后显示 n allocs/op 也就是内存分配了n次,m B/op, 总共分配了 m 字节8003641 B/op == 8 003 641 B=7.63M |
-cpuprofile=./cpu.prof | 生成CPU性能分析 | 执行 CPU profiling,并把结果保存在 cpu.prof |
-memprofile=./mem.prof | 生成内存性能分析 | 执行 Mem profiling,并把结果保存在 cpu.prof 文件中 |
四、执行结果说明
mac@fiveyoboy learn-go % go test -bench='BenchmarkStringBuilderAdd' -cpu=2,4 -count=3 -benchmem .
goos: darwin // 操作系统
goarch: arm64 // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu
// 测试函数-使用CPU核数 执行次数 每次耗时ns(左移9位为秒) 分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringBuilderAdd-2 2170213 546.5 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-2 2217442 548.2 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-2 2229522 543.1 ns/op 4320 B/op 6 allocs/op
// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringBuilderAdd-4 2068552 563.3 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-4 2069716 561.8 ns/op 4320 B/op 6 allocs/op
BenchmarkStringBuilderAdd-4 2103303 587.5 ns/op 4320 B/op 6 allocs/op
PASS
ok fiveyoboy/learn-go 9.302s
结果参数 | 简介 | |
---|---|---|
b.N | benchmark的次数b.N,默认是-benchtime=1s内执行的次数,b.N 从 -benchtime=1s开始,如果该用例能够在 1s 内完成,b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快 |
|
goos | 操作系统 | |
goarch | cpu处理器 | |
pkg | 所在的包 | |
BenchmarkGenerate-2 BenchmarkGenerate-4 BenchmarkGenerate-8 |
【测试函数名】-【cpu核数】BenchmarkGenerate-2, 表示使用2核CPU执行BenchmarkGenerate测试函数 |
|
ns/op | 每次耗时多少毫秒 | |
B/op | 每次分配内存多少字节Bytes | |
allocs/op | 每次分配多少次内存 |
其他
为了提高基准测试的准确度(如果基准测试在循环前需要一些耗时的配置,则可以先重置定时器):
- StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,我们也可以在调用 StopTimer 之后恢复计时;
- StopTimer:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;
- ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。本节开头有使用示例。
func BenchmarkStringBuilderAdd(b *testing.B) {
before() // 需要执行,但是又不希望参与基准测试耗时计算
b.ResetTimer() // 从现在开始计时
for n := 0; n < b.N; n++ {
StringBuilderAdd()
}
}
关于 go 如何高效的拼接字符串,请移步文章:
go 如何高效的拼接字符串)ng)fiveyoboy.com/articles/go-concat-string/)