尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Go 用 testing.B 写基准测试:b.ResetTimer、b.ReportAllocs 与别让编译器把你的代码优化掉

Go 用 testing.B 写基准测试:b.ResetTimer、b.ReportAllocs 与别让编译器把你的代码优化掉 Go 用 testing.B 写基准测试:b.ResetTimer、b.ReportAllocs 与别让编译器把你的代码优化掉写完一段自认为很快的 Go 代码,想量化到底有多快,很多人第一反应是自己time.Now()掐表跑一遍循环。结果测出来的数字忽大忽小,还没法和同事对齐。其实 Go 标准库自带了一套严谨的基准测试框架testing.B,它会自动决定跑多少轮、把结果换算成每次操作的耗时,还能顺带统计内存分配。这篇就把基准测试从「能跑」写到「测得准」。一个最小的基准测试基准测试函数以Benchmark开头,放在_test.go文件里,参数是*testing.B。核心是那个for i : 0; i b.N; i循环——b.N由框架动态决定,你不用管它是多少。// strjoin_test.gopackagestrjoinimport(stringstesting)funcBenchmarkStringPlus(b*testing.B){words:[]string{go,is,fast,and,simple}fori:0;ib.N;i{s:for_,w:rangewords{sw// 每次 都新分配一个字符串}_s}}funcBenchmarkStringBuilder(b*testing.B){words:[]string{go,is,fast,and,simple}fori:0;ib.N;i{varsb strings.Builderfor_,w:rangewords{sb.WriteString(w)}_sb.String()}}跑起来:gotest-bench.-benchmem-bench.表示跑所有基准(参数是正则,-benchBuilder只跑名字含 Builder 的);-benchmem打开内存统计。输出大概长这样:BenchmarkStringPlus-8 8123456 142.3 ns/op 48 B/op 4 allocs/op BenchmarkStringBuilder-8 19876543 60.1 ns/op 24 B/op 2 allocs/op-8是 GOMAXPROCS;ns/op是每次操作纳秒数,越小越快;B/op是每次操作分配的字节;allocs/op是分配次数。一眼看出 Builder 快一倍多、分配也少一半。b.N 是什么:为什么不能自己写死循环次数框架会先用很小的b.N(比如 1)试跑,根据耗时不断加大b.N,直到总运行时间够长(默认约 1 秒),这样统计才稳定。所以你的代码必须严格用b.N控制循环次数,自己写死for i : 0; i 1000000; i会让框架的自适应逻辑失效,测出来的数字毫无意义。坑一:初始化开销污染了结果,用 b.ResetTimer如果测试前有一段昂贵的准备工作(建大 map、读文件、造测试数据),它会被算进计时里,把结果拉偏。用b.ResetTimer()把计时器归零,只测你真正关心的部分。funcBenchmarkLookup(b*testing.B){// 造 100 万条数据是准备工作,不该计入m:make(map[int]int,1_000_000)fori:0;i1_000_000;i{m[i]i*2}b.ResetTimer()// 关键:把上面建 map 的耗时从计时里剔掉fori:0;ib.N;i{_m[i%1_000_000]}}配套的还有b.StopTimer()/b.StartTimer(),用于循环内部每轮都要重新造数据、但又不想把造数据算进去的场景:funcBenchmarkProcess(b*testing.B){fori:0;ib.N;i{b.StopTimer()data:makeFreshInput()// 每轮都要新数据,但不计时b.StartTimer()process(data)// 只测这一行}}注意StopTimer/StartTimer每轮都调有额外开销,数据准备很轻时反而得不偿失,能用ResetTimer就别用它。坑二:编译器把你的代码整个优化掉了这是基准测试最阴险的陷阱。如果计算结果没被使用,Go 编译器会判定这段代码是「死代码」直接删掉,你测出个0.3 ns/op还以为自己写出了世界最快函数。// 错误:result 没被用,整个 Abs 调用可能被优化掉funcBenchmarkAbsWrong(b*testing.B){fori:0;ib.N;i{Abs(-3.14)// 结果被丢弃,编译器可能直接删掉}}正确做法:把结果赋值给一个包级变量(逃逸到堆,编译器不敢删),循环外再消费一次。varresultfloat64// 包级变量,阻止编译器消除funcBenchmarkAbs(b*testing.B){varrfloat64fori:0;ib.N;i{rAbs(-3.14)// 结果被局部变量接住}resultr// 逃逸到包级变量,确保调用不被优化掉}判断有没有被优化掉的经验法则:如果一个明明有计算量的函数测出接近 0 或诡异地稳定,八成是被删了,检查结果有没有真正被消费。用 b.ReportAllocs() 强制显示分配,不依赖命令行-benchmem是全局开关,如果你想让某个基准无论命令行加不加都报告内存,在函数里调一次b.ReportAllocs():funcBenchmarkJSONMarshal(b*testing.B){b.ReportAllocs()// 这个基准总是报告 B/op 和 allocs/opv:map[string]int{a:1,b:2,c:3}b.ResetTimer()fori:0;ib.N;i{_,_json.Marshal(v)}}allocs/op往往比ns/op更值得盯:分配次数直接决定 GC 压力,减少分配通常是 Go 性能优化里回报最高的一环。让结果可信:多跑几轮再对比单次基准会受机器负载波动影响。要下「A 比 B 快」的结论,用-count跑多轮,再用官方工具benchstat做统计显著性判断:# 每个基准跑 10 轮,存到文件gotest-bench.-benchmem-count10new.txt# 对比改动前后(需要 go install golang.org/x/perf/cmd/benchstatlatest)benchstat old.txt new.txtbenchstat会给出均值、波动范围(±%)和差异是否显著。只看一次输出就说「优化了 5%」是不靠谱的,那点差距可能全在噪声里。小结基准函数以Benchmark开头,循环次数必须用b.N,别写死;跑用go test -bench. -benchmem。昂贵的一次性准备用b.ResetTimer()剔除;循环内每轮准备数据用StopTimer/StartTimer,但能避免就避免。结果一定要被消费(赋给包级变量),否则编译器把整段代码优化掉,数字失真。盯allocs/op往往比盯ns/op更有价值,减少分配就是减 GC 压力。下结论前用-count10benchstat判断差异是否显著。一句话记忆:b.N 让框架决定跑多少,ResetTimer 决定测什么,消费结果决定测的是不是真的在跑。
返回列表