mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
6.2 KiB
6.2 KiB
基准测试方法
编写基准测试
Go 基准测试使用 testing.B 类型,位于 _test.go 文件中。
基准测试函数名必须以 Benchmark 开头。
func BenchmarkStrconv(b *testing.B) {
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
_ = s
}
}
func BenchmarkFmtSprint(b *testing.B) {
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
_ = s
}
}
关键规则:
- 使用
b.N作为循环边界——框架会调整它以获得稳定的计时 - 将结果赋值给变量(或
_),防止编译器优化掉调用 - 在不需要测量的昂贵设置之后使用
b.ResetTimer() - 使用
b.ReportAllocs()或-benchmem标志跟踪分配情况
子基准测试
func BenchmarkConvert(b *testing.B) {
for _, size := range []int{10, 100, 1000} {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := make([]byte, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = string(data)
}
})
}
}
运行基准测试
# 运行包中的所有基准测试
go test -bench=. ./...
# 运行特定基准测试并显示内存统计
go test -bench=BenchmarkStrconv -benchmem ./...
# 多次运行以获得统计显著性
go test -bench=. -benchmem -count=10 ./...
-benchmem 标志报告每次操作的分配次数。-count 标志将每个基准测试运行 N 次以获得统计显著性。
解读结果
BenchmarkStrconv-8 18705042 64.2 ns/op 16 B/op 1 allocs/op
BenchmarkFmtSprint-8 8249536 143.0 ns/op 16 B/op 2 allocs/op
| 字段 | 含义 |
|---|---|
-8 |
GOMAXPROCS |
18705042 |
迭代次数 |
64.2 ns/op |
每次操作时间 |
16 B/op |
每次操作分配的字节数 |
1 allocs/op |
每次操作的堆分配次数 |
使用 benchstat 进行比较
benchstat 对基准测试结果进行统计比较。安装它并将基准测试输出保存到文件:
# 安装 benchstat
go install golang.org/x/perf/cmd/benchstat@latest
# 运行基准测试并保存结果
go test -bench=. -benchmem -count=10 ./... > old.txt
# 进行修改后再次运行
go test -bench=. -benchmem -count=10 ./... > new.txt
# 比较结果
benchstat old.txt new.txt
解读 benchstat 输出
name old time/op new time/op delta
Strconv-8 64.2ns ± 2% 61.8ns ± 1% -3.74% (p=0.001 n=10+10)
- delta:变化百分比(负数 = 更快)
- p-value:统计显著性(p < 0.05 为显著)
- n:使用的有效样本数量
提示:
- 始终使用
-count=10或更高以获得可靠结果 - 小的 p 值确认变化是真实的,而非噪声
- 如果 benchstat 显示
~(波浪号),则差异不具有统计显著性
来自性能模式的基准测试示例
strconv vs fmt
| 方式 | 速度 | 分配次数 |
|---|---|---|
fmt.Sprint |
143 ns/op | 2 allocs/op |
strconv.Itoa |
64.2 ns/op | 1 allocs/op |
重复字节转换
func BenchmarkRepeatedConversion(b *testing.B) {
var buf bytes.Buffer
for i := 0; i < b.N; i++ {
buf.Write([]byte("Hello world"))
}
}
func BenchmarkSingleConversion(b *testing.B) {
var buf bytes.Buffer
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
buf.Write(data)
}
}
| 方式 | 速度 |
|---|---|
| 重复转换 | 22.2 ns/op |
| 单次转换 | 3.25 ns/op |
Slice 容量
func BenchmarkNoCapacity(b *testing.B) {
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < 1000; k++ {
data = append(data, k)
}
}
}
func BenchmarkWithCapacity(b *testing.B) {
for n := 0; n < b.N; n++ {
data := make([]int, 0, 1000)
for k := 0; k < 1000; k++ {
data = append(data, k)
}
}
}
| 方式 | 时间(1 亿次迭代) |
|---|---|
| 无容量 | 2.48s |
| 指定容量 | 0.21s |
使用 pprof 进行性能分析
使用 pprof 在优化前识别瓶颈。基准测试衡量改进效果;pprof 找到需要改进的地方。
CPU 性能分析
# 从基准测试生成 CPU 分析文件
go test -bench=BenchmarkHotPath -cpuprofile=cpu.prof ./...
# 使用 pprof 分析
go tool pprof cpu.prof
常用 pprof 命令:
(pprof) top10 # 按 CPU 时间排列的前 10 个函数
(pprof) list funcName # 某个函数的带注释源码
(pprof) web # 浏览器中的交互式图表
内存性能分析
# 生成内存分析文件
go test -bench=BenchmarkHotPath -memprofile=mem.prof ./...
# 分析分配情况
go tool pprof -alloc_space mem.prof
运行中服务的 HTTP 性能分析
import _ "net/http/pprof"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... 应用程序代码 ...
}
通过 http://localhost:6060/debug/pprof/ 访问性能分析数据。
性能分析工作流
- 对疑似热点路径进行基准测试
- 使用 pprof 分析以确认时间花在了哪里
- 使用本技能中的模式进行优化
- 重新基准测试以用 benchstat 验证改进
- 重新分析以检查是否出现新的瓶颈
常见错误
忽略 b.N
测试框架会调整 b.N 以获得稳定的计时。使用固定迭代次数会产生无意义的结果:
// 不好:忽略 b.N —— 基准测试框架无法校准
func BenchmarkFixed(b *testing.B) {
for i := 0; i < 1000; i++ {
doWork()
}
}
// 好:使用 b.N 作为循环边界
func BenchmarkCorrect(b *testing.B) {
for i := 0; i < b.N; i++ {
doWork()
}
}
未防止编译器优化消除
如果函数调用的结果未被使用,编译器可能会完全优化掉该调用。将结果赋值给包级变量:
// 不好:编译器可能会优化掉调用
func BenchmarkElided(b *testing.B) {
for i := 0; i < b.N; i++ {
expensiveFunc()
}
}
// 好:赋值给包级变量以防止优化消除
var benchResult int
func BenchmarkKept(b *testing.B) {
var r int
for i := 0; i < b.N; i++ {
r = expensiveFunc()
}
benchResult = r
}