Slide 1

Slide 1 text

Go1.24で testing.B.Loopが爆誕 2025/03/20 Kyoto.go remote #59 kuro 1

Slide 2

Slide 2 text

1. 登場した背景 - なぜtesting.B.Loop が必要だったのか 2. b.Loop() の実装 - 実際のコード 3. メリット - testing.B.Loop の利点 4. まとめ - おわりに 目次 2

Slide 3

Slide 3 text

従来のベンチマーク func BenchmarkTraditional(b *testing.B) { // セットアップコード data := prepareExpensiveData(10000) // タイマーをリセットする必要がある b.ResetTimer() // b.Nを使ってループ for i := 0; i < b.N; i++ { result := processData(data) } } 登場した背景 3

Slide 4

Slide 4 text

従来のベンチマーク(b.N )の問題点  (issue #61515 より) b.N の誤用が多い 多くのベンチマークがb.N の使用を忘れている(rsc/badbench.md) イテレーションカウントではなく入力サイズとして使われることも タイマーの管理 ResetTimer を適切な場所で呼ばないと、ベンチマーク結果が不正確になる。 コンパイラ最適化による影響 b.N のループ内でコンパイラが最適化(インライン展開)を行うと、測定結果が意図しない ものになる可能性がある。 登場した背景 4

Slide 5

Slide 5 text

コンパイラ最適化による影響? インライン展開とは インライン展開(インラインてんかい、英: inline expansion または 英: inlining ) とは、コンパイラによる最適化手法の1 つで、関数を呼び出す側に呼び出される関 数のコードを展開し、関数への制御転送をしないようにする手法。これにより関 数呼び出しに伴うオーバーヘッドを削減する。特に小さくて頻繁に呼ばれる関数 では効果的であり、呼び出し側にそのコードを展開することで定数畳み込みなど のさらなる最適化を施せる可能性が生じる。 (Wikipedia インライン展開 より引 用) 「関数を呼び出す側に関数のコードを展開することで、オーバーヘッドを削減する。 」こと らしい。 登場した背景 5

Slide 6

Slide 6 text

コンパイラ最適化による影響? インライン展開される条件  Go Wiki: Compiler And Runtime Optimizations より function should be simple enough, the number of AST nodes must less than the budget (80); function doesn't contain complex things like closures, defer, recover, select, etc; function isn't prefixed by go:noinline; function isn't prefixed by go:uintptrescapes, since the escape information will be lost during inlining; function has body; etc. 登場した背景 6

Slide 7

Slide 7 text

func BenchmarkWithLoop(b *testing.B) { // セットアップコード data := prepareExpensiveData(10000) // タイマーは自動的に管理される for b.Loop() { processData(data) } } Go1.24 以降で可能なベンチマークコード 7

Slide 8

Slide 8 text

func (b *B) Loop() bool { if b.loopN != 0 && b.loopN < b.N { b.loopN++ return true } return b.loopSlowPath() } src/testing/benchmark.go b.Loop() の実装 8

Slide 9

Slide 9 text

func (b *B) loopSlowPath() bool { if b.loopN == 0 { b.N = 1 b.loopN = 1 b.ResetTimer() // 初回の実行時のみタイマーをリセット return true } if b.benchTime.n > 0 { if b.N < b.benchTime.n { b.N = b.benchTime.n b.loopN++ return true } b.StopTimer() // ループ回数がb.Nを超えたらタイマーを停止 return false } return b.stopOrScaleBLoop() } 9

Slide 10

Slide 10 text

compile 側で for b.Loop() を検出して制御する 612043: cmd/compile: keep variables alive in testing.B.Loop loops | https://go- review.googlesource.com/c/go/+/612043 どのようにしてインライン展開を防ぐのか? 10

Slide 11

Slide 11 text

func (s *inlClosureState) mark(n ir.Node) ir.Node { (--中略--) if isTestingBLoop(n) { // No inlining nor devirtualization performed on b.Loop body if base.Flag.LowerM > 1 { fmt.Printf("%v: skip inlining within testing.B.loop for %v\n", ir.Line(n), n) } // We still want to explore inlining opportunities in other parts of ForStmt. nFor, _ := n.(*ir.ForStmt) nForInit := nFor.Init() for i, x := range nForInit { if x != nil { nForInit[i] = s.mark(x) } } if nFor.Cond != nil { nFor.Cond = s.mark(nFor.Cond) } if nFor.Post != nil { nFor.Post = s.mark(nFor.Post) } return n } (--以下略--) src/cmd/compile/internal/inline/interleaved/interleaved.go どのようにしてインライン展開を防ぐのか? 11

Slide 12

Slide 12 text

1. b.N の誤用が減る 2. タイマーの自動管理 b.ResetTimer() の記述が不要になる ループ開始時にタイマーの自動リセット、終了時に自動停止 3. コンパイラ最適化の抑制 for b.Loop() の中ではインライン展開が抑制される testing.B.Loopのメリット 12

Slide 13

Slide 13 text

・Go 1.24では新しいベンチマーク方法としてtesting.B.Loopが導入 タイマーが自動的に管理( b.ResetTimer() の記述が完全に不要に) コンパイラ最適化から保護 ・シンプルにfor b.Loop() を使うのが良さそう 既存コードも for i := 0; i < b.N; i++ → for b.Loop() まとめ 13