Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go1.24で testing.B.Loopが爆誕

kuro
March 20, 2025

Go1.24で testing.B.Loopが爆誕

Kyoto.go remote #59 でLTをしたときに使ったスライドです。

kuro

March 20, 2025
Tweet

More Decks by kuro

Other Decks in Programming

Transcript

  1. 1. 登場した背景 - なぜtesting.B.Loop が必要だったのか 2. b.Loop() の実装 - 実際のコード

    3. メリット - testing.B.Loop の利点 4. まとめ - おわりに 目次 2
  2. 従来のベンチマーク func BenchmarkTraditional(b *testing.B) { // セットアップコード data := prepareExpensiveData(10000)

    // タイマーをリセットする必要がある b.ResetTimer() // b.Nを使ってループ for i := 0; i < b.N; i++ { result := processData(data) } } 登場した背景 3
  3. 従来のベンチマーク(b.N )の問題点  (issue #61515 より) b.N の誤用が多い 多くのベンチマークがb.N の使用を忘れている(rsc/badbench.md) イテレーションカウントではなく入力サイズとして使われることも

    タイマーの管理 ResetTimer を適切な場所で呼ばないと、ベンチマーク結果が不正確になる。 コンパイラ最適化による影響 b.N のループ内でコンパイラが最適化(インライン展開)を行うと、測定結果が意図しない ものになる可能性がある。 登場した背景 4
  4. コンパイラ最適化による影響? インライン展開とは インライン展開(インラインてんかい、英: inline expansion または 英: inlining ) とは、コンパイラによる最適化手法の1

    つで、関数を呼び出す側に呼び出される関 数のコードを展開し、関数への制御転送をしないようにする手法。これにより関 数呼び出しに伴うオーバーヘッドを削減する。特に小さくて頻繁に呼ばれる関数 では効果的であり、呼び出し側にそのコードを展開することで定数畳み込みなど のさらなる最適化を施せる可能性が生じる。 (Wikipedia インライン展開 より引 用) 「関数を呼び出す側に関数のコードを展開することで、オーバーヘッドを削減する。 」こと らしい。 登場した背景 5
  5. コンパイラ最適化による影響? インライン展開される条件  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
  6. func BenchmarkWithLoop(b *testing.B) { // セットアップコード data := prepareExpensiveData(10000) //

    タイマーは自動的に管理される for b.Loop() { processData(data) } } Go1.24 以降で可能なベンチマークコード 7
  7. 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
  8. 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
  9. compile 側で for b.Loop() を検出して制御する 612043: cmd/compile: keep variables alive

    in testing.B.Loop loops | https://go- review.googlesource.com/c/go/+/612043 どのようにしてインライン展開を防ぐのか? 10
  10. 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