0.3128 ns/op。1000個の整数を合計するベンチが返すこの数字は、測定の失敗を意味します。合計が0.3ナノ秒で終わるはずはなく、コンパイラが処理ごと消したサイン。この誤計測は、Go 1.24で入った testing.B.Loop で防げます。
b.Nループが測り損なう原因
昔ながらの b.N ループで、1000要素の []int を合計する関数を測ってみます。
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
var data = make([]int, 1000)
func BenchmarkSumNaive(b *testing.B) {
for range b.N {
sum(data)
}
}
結果はこうなります。
goos: darwin
goarch: arm64
pkg: example/bench
cpu: Apple M2
BenchmarkSumNaive-8 1000000000 0.3128 ns/op
PASS
1000回の加算が0.3ナノ秒。反復回数も10億回で頭打ち。物理的にありえない値です。原因は、公式ブログ “More predictable benchmarking with testing.B.Loop”(Junyang Shao, 2025年4月)が “Old benchmark loop problems” の節でまとめています。
計測したいコードが丸ごと消える
Goコンパイラは、戻り値を使わない純粋な関数呼び出しをデッドコードとして削除します。sum はインライン化できる小さな関数。戻り値がどこにも使われないと分かれば、加算ループごと消えます。残るのは空の for だけ。だから0.3ナノ秒。
コンパイラは観測可能な挙動を変えない範囲で最適化してよく、戻り値を捨てる呼び出しは観測不能とみなされます。実際に消えたかは go test -gcflags=-m で追える。inlining call to sum と出たあと、その結果が使われなければ最適化で落ちます。
従来はこれを防ぐため、パッケージ変数に結果を代入する「sink」を挟んでいました。
var sink int
func BenchmarkSumSink(b *testing.B) {
for range b.N {
sink = sum(data)
}
}
BenchmarkSumSink-8 3820000 313.5 ns/op
313ナノ秒。これが本当の値です。ただしsinkはベンチのたびに書く定型で、書き忘れれば静かに0.3ナノ秒へ逆戻り。sinkを書き忘れたまま「この実装は速い」と報告しかけ、レビューで桁を指摘された経験があります。
セットアップの時間がタイマーに乗る
もう一つはタイマー。b.N ループでは、ループ前に書いた重い準備処理も計測時間に入り込む。避けるには b.ResetTimer()、途中で止めるには b.StopTimer() を手で呼びます。呼び忘れると準備コストが1反復あたりの数字へ混ざり、結果が水増しされます。
for b.Loop() に書き換える
func BenchmarkSumLoop(b *testing.B) {
for b.Loop() {
sum(data)
}
}
BenchmarkSumLoop-8 3818766 313.5 ns/op
sinkも ResetTimer も無しで313ナノ秒。素朴な b.N 版とほぼ1文字違いなのに、正しい値が出ます。for range b.N を for b.Loop() に替えるだけ。
タイマーは自動でリセット・停止される
Loop は初回の呼び出しでベンチマークタイマーをリセットします。ループに入る前の準備は計測対象から外れる。false を返すときにはタイマーを止めるので、ループ後のクリーンアップも計測されません。ResetTimer と StopTimer の定型呼び出しが要らなくなります。
Go 1.24で追加、現行では推奨
シグネチャは func (b *B) Loop() bool。Go 1.24.0で追加され、2026年時点の現行はGo 1.26系。公式ドキュメントは b.Loop を “the preferred way to write benchmarks” と位置づけています。b.N ループも引き続き有効で、廃止されたわけではありません。
デッドコード削除を止める仕組み
書き換えるだけで削除が止まる理由は、公式ブログの “How testing.B.Loop helps” 節にあります。
ループ本文のインライン化を止める
the Go compiler now detects loops where the condition is just a call to testing.B.Loop and prevents dead code elimination within the loop. In Go 1.24, this is implemented by disallowing inlining into the body of such a loop.
ループ条件が b.Loop() だとコンパイラが認識すると、その本文へのインライン化を止める。インライン化されなければ戻り値の追跡もできず、削除の判断が働きません。pkg.go.dev の testing 側の記述では、ループ本文の引数・戻り値・代入変数が runtime.KeepAlive 相当で生かされる、とあります。効くのはループの波括弧の中の文だけ。
ベンチ関数の呼び出しは1回だけ
b.N 方式では、テストパッケージが b.N を増やしながらベンチ関数を何度も呼びます。準備とクリーンアップもそのたびに走る。対して b.Loop は、時間のしきい値に達するまでループを回すだけです。公式の表現で “only needs to call the benchmark function once”。重いセットアップが1回で済むぶん、”b.Loop-style benchmarks even complete in less time” とも書かれています。
効くのは準備が重いベンチ。10万件をDBへ流し込んでから検索を測る場合、b.N 方式は投入処理を b.N 決定のたびに繰り返します。b.ResetTimer で計測から外しても、実際に流し込む時間そのものは縮みません。b.Loop なら投入は1回。ベンチ全体の待ち時間が短くなります。
b.N と b.Loop の違い早見表
観点ごとの差はこうなります。
| 観点 | for range b.N | for b.Loop() |
|---|---|---|
| タイマー制御 | 手動(ResetTimer / StopTimer) | 自動でリセット・停止 |
| デッドコード削除 | 起きうる(sink変数で自衛) | インライン抑止で保護 |
| ベンチ関数の呼び出し | Nを変えて複数回 | 1回 |
| ループ前の b.N | 反復数として使える | 0(測定後に確定) |
| 導入 | 以前から | Go 1.24〜 |
| 位置づけ | 引き続き有効 | 推奨 |
新規ベンチは b.Loop でよく、既存の b.N ベンチを急いで書き換える必要はありません。
書き換えでハマるところ
ループ前の b.N は 0
b.N 方式では、ループ前に make([]T, b.N) で結果領域を確保する書き方ができました。b.Loop では反復数が事前に分かりません。b.N が確定するのはループを抜けたあと。ループ前に読むと 0 です。
func BenchmarkN(b *testing.B) {
b.Logf("ループ前 b.N=%d", b.N)
for b.Loop() {
}
b.Logf("ループ後 b.N=%d", b.N)
}
=== RUN BenchmarkN
n_test.go:2: ループ前 b.N=0
n_test.go:5: ループ後 b.N=48219753
BenchmarkN-8 48219753 24.71 ns/op
反復数に依存した事前確保をしていたベンチは、ループ内で確保するか設計を見直します。ただし毎反復の make は割り当てを増やし、計測対象そのものを変えかねません。確保はループ外に固定サイズで置き、中身だけ書き換えるのが無難。抜けたあとの b.N は総反復数として、1件あたりの平均バイト数のような独自指標の計算に使えます。
条件は b.Loop() と正確に書く
削除防止が効くのは、ループ条件が b.Loop() という形のときだけ。戻り値を変数に受けてから判定すると、コンパイラの特別扱いから外れます。
// 保護が外れる書き方(戻り値を変数に受けている)
for ok := b.Loop(); ok; ok = b.Loop() {
sum(data)
}
// 正しい書き方
for b.Loop() {
sum(data)
}
# 変数に受けた版 → また消えて 0.3ns/op
BenchmarkAssign-8 1000000000 0.3129 ns/op
# 直接条件に書いた版 → 本当の値
BenchmarkDirect-8 3820001 313.4 ns/op
公式ドキュメントも “the loop condition must be written exactly as b.Loop()” と明記しています。凝った書き方をせず、素直に for b.Loop() と書くのが安全です。
b.N ループとの併用は不可
ベンチ関数の中に b.Loop ループと b.N ループを同居させることはできません。”When to use” 節に “a b.N-style loop cannot coexist with a b.Loop-style loop” とあります。ループは1つだけ。各反復が同じ処理を繰り返す前提です。
反復ごとに準備が変わるとき
各反復で毎回ちがう入力を作りたい場合、その準備を計測から外す責任は書き手に残ります。ソート対象を毎回シャッフルするベンチが典型。準備をループ内に置いたまま StopTimer を忘れると、シャッフルの時間がソートの数字へ足し込まれます。
func BenchmarkSortInts(b *testing.B) {
ints := make([]int, 1024)
for b.Loop() {
b.StopTimer()
fillRandomInts(ints)
b.StartTimer()
slices.Sort(ints)
}
}
BenchmarkSortInts-8 32551 36420 ns/op
b.StopTimer() と b.StartTimer() で fillRandomInts の時間を除外します。この形は公式ブログの “When to use” 節が挙げている例と同じ。b.Loop にしても、ループ内のタイマー管理まで自動になるわけではありません。
メモリ割り当ても一緒に測る
時間だけでなく、1操作あたりのメモリ割り当ても同じベンチで取れます。b.Loop ループはそのまま、割り当て統計を有効にするだけ。
b.ReportAllocs か -benchmem で出す
ベンチ関数の中で b.ReportAllocs() を呼ぶか、実行時に -benchmem を付けます。公式ドキュメントは ReportAllocs を “enables malloc statistics for this benchmark” と説明し、”equivalent to setting -test.benchmem” と補足しています。前者はそのベンチ関数だけ、後者はパッケージ内の全ベンチに効く。
func BenchmarkJoin(b *testing.B) {
parts := []string{"a", "b", "c", "d", "e"}
b.ReportAllocs()
for b.Loop() {
_ = strings.Join(parts, "/")
}
}
$ go test -bench=Join
BenchmarkJoin-8 18240000 64.30 ns/op 16 B/op 1 allocs/op
ns/op に加えて B/op(1操作あたりのバイト数)と allocs/op(割り当て回数)が並びます。strings.Join は5要素の連結で16バイトを1回。ここが 0 allocs/op に減れば、割り当てを削れた証拠になります。
数字を安定させる回し方
1回の実行では値がぶれます。-count で複数回まわし、benchstat で中央値とばらつきを見るのが定番です。
$ go test -bench=Join -benchmem -count=6 | benchstat -
│ stdin │
│ sec/op │
Join-8 64.11n ± 2%
± 2% は6回の測定のばらつき。数%を超えて揺れるなら、バックグラウンド負荷や周波数変動を疑います。b.Loop で計測範囲が締まっても、環境ノイズまで消えるわけではありません。
まとめ
testing.B.Loop は、b.N ベンチが抱えていた計測ミスを構造的に減らします。
for range b.Nをfor b.Loop()に替えるだけで、タイマーのリセット・停止が自動になる- ループ本文はインライン化が抑止され、戻り値を捨ててもデッドコード削除で消えない。sink変数が不要
- ベンチ関数の呼び出しは1回。重いセットアップの繰り返しが減り、実行時間も短くなる
- ループ前の
b.Nは0。反復数に依存した事前確保は要見直し - 条件は
b.Loop()と正確に書く。b.Nループとの併用は不可 - 反復ごとの準備は
StopTimer/StartTimerで自分で外す
既存の b.N ベンチは、触るファイルのベンチから順に for b.Loop() へ寄せ、sinkや ResetTimer の定型を消していくのが現実的。急いで全部を書き換える必要はありません。
テストコード側の書き分けは Goテストの書き方 でも整理しています。並行処理を決定的に測る話は testing/synctest が近いテーマです。

