登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go benchmark 怎么统计内存分配次数

来源:17golang原创

时间:2026-09-05 23:49:39 219浏览 收藏

Go benchmark 想统计内存分配次数,最短路径是运行 go test -benchmem,结果里的 allocs/op 就是每次操作平均发生的分配次数;同时出现的 B/op 是每次操作分配的字节数。若只想让某一个基准函数开启统计,可以调用 b.ReportAllocs()。需要在 Go 代码里拿到数字,则用 testing.Benchmark 返回的 BenchmarkResult.AllocsPerOp(),或对独立函数使用 testing.AllocsPerRun

要点速览
  • -benchmem 适合从命令行快速比较多个 benchmark。
  • b.ReportAllocs() 只打开当前 benchmark 的分配统计,不会改变其他基准。
  • allocs/op 是平均值;要避免把预热、初始化和并行调度误算进目标操作。

先用 -benchmem 看见 allocs/op

准备一个以 _test.go 结尾的文件。下面的例子刻意把输入放在基准函数外面,循环里只保留需要比较的字符串转换;这样测量目标更清楚。

package demo

import (
	"strconv"
	"testing"
)

var benchmarkSink string

func BenchmarkFormatID(b *testing.B) {
	for b.Loop() {
		// 把结果写入包级变量,避免编译器把转换优化掉。
		benchmarkSink = strconv.Itoa(20260905)
	}
}

在包目录执行:

go test -run '^$' -bench '^BenchmarkFormatID$' -benchmem
# -run '^$' 跳过普通测试;-benchmem 打开分配统计。
# -bench 用正则只选择目标基准,减少无关输出。

输出通常会同时包含 ns/opB/opallocs/op。其中 allocs/op 不是“整个进程从启动到退出的分配总数”,而是基准框架根据测量期间的总分配次数除以操作次数得到的平均指标。参数关系可以这样记:

指标回答的问题怎么用
allocs/op一次操作平均分配几次看对象数量和逃逸带来的分配频率
B/op一次操作平均分配多少字节看分配体积,和次数一起比较
ns/op一次操作平均耗时判断分配是否已经影响时间
Go testing.B benchmark 的测量边界、循环操作与 allocs/op 和 B/op 指标关系结构图
图1:benchmark 测量边界把循环中的目标操作连接到 allocs/op 与 B/op 两个内存指标。

只统计当前基准时调用 b.ReportAllocs

如果不想要求每次命令都记得加 -benchmem,可以把统计开关写在基准函数中。官方文档说明,ReportAllocs 等价于设置 -test.benchmem,但只影响调用它的 benchmark。

func BenchmarkFormatIDWithReport(b *testing.B) {
	// 只让这个基准输出内存分配指标。
	b.ReportAllocs()
	for b.Loop() {
		// 循环内保持和其他方案相同的工作量,才能公平比较。
		benchmarkSink = strconv.Itoa(20260905)
	}
}

这时即使只运行 go test -run '^$' -bench BenchmarkFormatIDWithReport,结果也会带上分配指标。需要注意的是,放在循环前的初始化通常不应算作一次操作的成本;如果使用旧式的 for i := 0; i 写法,昂贵初始化可能在 benchmark 调整 b.N 时重复执行,应该用 b.ResetTimer() 明确清零计时和分配计数。新代码优先采用 b.Loop(),它由 benchmark 框架管理迭代边界。

需要在 Go 代码里读取次数怎么办

命令行适合人工查看,自动回归或阈值判断则可以调用 testing.Benchmark。它返回 BenchmarkResult,其中 AllocsPerOp() 的含义就是总内存分配次数除以操作次数。

package demo

import (
	"fmt"
	"strconv"
	"testing"
)

func allocationsPerOperation() int64 {
	result := testing.Benchmark(func(b *testing.B) {
		b.ReportAllocs()
		for b.Loop() {
			// 让被测结果存活,避免示例只测到无效计算。
			benchmarkSink = strconv.Itoa(20260905)
		}
	})
	return result.AllocsPerOp()
}

func Example_allocationsPerOperation() {
	// 返回值适合用于回归门槛或记录到自定义测试报告。
	fmt.Println(allocationsPerOperation() >= 0)
	// Output: true
}

AllocsPerOp 返回的是整数型每操作指标;若你还想看字节数,可以调用同一个结果的 AllocedBytesPerOp()。不要把 BenchmarkResult.String() 当成完整内存报告:官方说明它不包含 allocs/opB/op,这两项由 MemString() 提供。

Go testing 包中 ReportAllocs、BenchmarkResult.AllocsPerOp 与 AllocsPerRun 的指标获取关系图
图2:从 benchmark 开关到程序化读取,三条 API 路径分别服务命令行、基准结果和独立函数测量。

只测一个函数时用 AllocsPerRun

有些场景不需要完整 benchmark,只想回答“调用这个函数一次平均分配几次”。这时 testing.AllocsPerRun(runs, f) 更直接:

func TestFormatAllocations(t *testing.T) {
	avg := testing.AllocsPerRun(100, func() {
		// 每轮只放被测调用,不把准备工作混进指标。
		benchmarkSink = strconv.Itoa(20260905)
	})
	if avg 

这里有两个容易忽略的语义。第一,函数会先执行一次预热,然后才统计指定次数;第二,测量期间会把 GOMAXPROCS 临时设为 1,并在返回前恢复。因此它适合测量稳定的单函数分配,不适合直接代表多 goroutine 生产流量下的分配行为。

如果 benchmark 使用 RunParallel,不要在各 goroutine 中分别输出同一个自定义计数器而不做同步;并且不要在并行体内调用 ResetTimer,因为它对全局 benchmark 有影响。想比较不同实现时,先统一输入、初始化位置和运行参数,再看 allocs/opB/op 是否同时下降。

常见问题

allocs/op 为 0 就代表完全没有内存分配吗?

它表示测量口径下平均每次操作没有观察到可计入的分配,不等于整个进程、测试初始化或其他 goroutine 都没有分配。

为什么加了 b.ReportAllocs 还是看不到 allocs/op?

确认运行的是该 benchmark,并检查函数名是否匹配 BenchmarkXxx。如果运行的是普通测试,报告不会自动变成 benchmark 输出。

应该比较 allocs/op 还是 B/op?

对象数量敏感的问题先看 allocs/op,大对象或缓冲区问题要同时看 B/op;性能回归还要把 ns/op 一起记录。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>