Files
OpenFlare/.agents/skills/go-performance/references/BENCHMARKS.md
T
2026-08-04 11:40:32 +08:00

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/ 访问性能分析数据。

性能分析工作流

  1. 对疑似热点路径进行基准测试
  2. 使用 pprof 分析以确认时间花在了哪里
  3. 使用本技能中的模式进行优化
  4. 重新基准测试以用 benchstat 验证改进
  5. 重新分析以检查是否出现新的瓶颈

常见错误

忽略 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
}