如何对Go函数编写基准测试(Benchmark)
2025/8/12大约 4 分钟
如何对Go函数编写基准测试(Benchmark)
基本概念
在Go中,基准测试是写在_test.go
文件中,以Benchmark
开头的函数,接受一个*testing.B
参数。
testing.B 字段
相关信息
字段名 | 类型 | 描述 |
---|---|---|
N | int | 基准测试的迭代次数,由测试框架自动调整以获取可靠的计时结果 |
Timer | 嵌套结构体 | 包含计时相关的方法和字段 |
bytes | int64 | 每次操作处理的字节数,用于计算吞吐量 |
startAllocs | uint64 | 测试开始时内存分配计数(内部使用) |
startBytes | uint64 | 测试开始时分配的字节数(内部使用) |
netAllocs | uint64 | 测试期间的内存分配总数(使用 ReportAllocs() 时显示) |
netBytes | uint64 | 测试期间分配的字节总数(使用 ReportAllocs() 时显示) |
result | BenchmarkResult | 存储基准测试结果的字段 |
parallelism | int | 并行度(用于 RunParallel ) |
context | *benchContext | 基准测试上下文(内部使用) |
hasSub | bool | 是否有子测试(内部使用) |
finished | bool | 测试是否已完成(内部使用) |
主要方法
方法名 | 描述 |
---|---|
ResetTimer() | 重置计时器,排除初始化时间 |
StartTimer() | 开始计时 |
StopTimer() | 暂停计时 |
ReportAllocs() | 启用内存分配统计 |
ReportMetric(float64, string) | 报告自定义指标 |
SetBytes(int64) | 设置每次操作处理的字节数 |
SetParallelism(int) | 设置并行度 |
Run(string, func(*B)) | 运行子基准测试 |
RunParallel(func(*PB)) | 并行运行基准测试 |
示例用法
func BenchmarkExample(b *testing.B) {
// 初始化代码(不计时)
data := make([]byte, 1024)
b.ResetTimer() // 重置计时器
b.ReportAllocs() // 报告内存分配
for i := 0; i < b.N; i++ {
// 被测代码
process(data)
b.SetBytes(int64(len(data))) // 设置处理的字节数
}
}
示例1: 简单的字符串拼接基准测试
假设我们有一个字符串拼接函数:
// string_util.go
package main
func ConcatenateStrings(a, b string) string {
return a + b
}
基准测试写法
// string_util_test.go
package main
import "testing"
func BenchmarkConcatenateStrings(b *testing.B) {
str1 := "Hello, "
str2 := "World!"
// 重置计时器,排除初始化时间
b.ResetTimer()
// 循环b.N次,测试框架会自动决定N的值
for i := 0; i < b.N; i++ {
_ = ConcatenateStrings(str1, str2)
}
}
运行基准测试
go test -bench=.
输出类似:
BenchmarkConcatenateStrings-8 100000000 10.2 ns/op
BenchmarkConcatenateStrings-8
: 测试名称,-8表示使用了8个CPU核心100000000
: 循环执行的次数10.2 ns/op
: 每次操作平均耗时10.2纳秒
示例2: 更复杂的切片操作基准测试
// slice_util.go
package main
func FilterEvenNumbers(numbers []int) []int {
var result []int
for _, num := range numbers {
if num%2 == 0 {
result = append(result, num)
}
}
return result
}
基准测试写法
// slice_util_test.go
package main
import (
"math/rand"
"testing"
)
func BenchmarkFilterEvenNumbers(b *testing.B) {
// 准备测试数据
rand.Seed(42)
numbers := make([]int, 10000)
for i := range numbers {
numbers[i] = rand.Intn(1000)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = FilterEvenNumbers(numbers)
}
}
进阶技巧:内存分配统计
func BenchmarkFilterEvenNumbersWithAllocs(b *testing.B) {
rand.Seed(42)
numbers := make([]int, 10000)
for i := range numbers {
numbers[i] = rand.Intn(1000)
}
b.ResetTimer()
b.Run("naive", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = FilterEvenNumbers(numbers)
}
})
b.Run("prealloc", func(b *testing.B) {
for i := 0; i < b.N; i++ {
// 预分配切片容量
result := make([]int, 0, len(numbers)/2)
for _, num := range numbers {
if num%2 == 0 {
result = append(result, num)
}
}
_ = result
}
})
}
运行时会显示内存分配情况:
BenchmarkFilterEvenNumbersWithAllocs/naive-8 5000 234567 ns/op 159744 B/op 1 allocs/op
BenchmarkFilterEvenNumbersWithAllocs/prealloc-8 10000 123456 ns/op 49152 B/op 1 allocs/op
示例3: 并行基准测试
对于可以并行执行的代码,可以使用RunParallel
方法:
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 并行执行的代码
_ = ConcatenateStrings("a", "b")
}
})
}
最佳实践
提示
- 隔离测试环境:每个基准测试应该独立,不受其他测试影响
- 重置计时器:使用
b.ResetTimer()
排除初始化时间 - 避免优化干扰:确保编译器不会优化掉你的测试代码(如使用
_ =
接收结果) - 多次运行:使用
go test -bench=. -count=5
获取更稳定的结果 - 内存分析:使用
-benchmem
标志查看内存分配情况 - 比较测试:使用子测试比较不同实现的性能
运行选项
相关信息
常用标志:
-bench=.
:运行所有基准测试-bench=Concatenate
:运行名称包含"Concatenate"的基准测试-benchmem
:显示内存分配统计-count=5
:运行5次取平均值-cpu=1,2,4
:使用不同CPU核心数测试