[インデックス 17540] ファイルの概要
このコミットは、Go言語のコマンドラインツール cmd/go
のテスト実行ロジック、特にベンチマークの実行順序に関する変更を含んでいます。対象ファイルは src/cmd/go/test.go
であり、go test
コマンドがテストやベンチマークを実行する際の内部的な依存関係の管理方法を定義しています。
コミット
- コミットハッシュ:
90f91928866a602df045b6c44f58a6f2f741bbf0
- 作者: Russ Cox rsc@golang.org
- 日付: Tue Sep 10 14:43:21 2013 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/90f91928866a602df045b6c44f58a6f2f741bbf0
元コミット内容
cmd/go: run benchmarks in sequence
Fixes #5662.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13650043
変更の背景
このコミットは、Go言語のIssue 5662「go test -bench
should run benchmarks sequentially」を修正するために行われました。
従来の go test -bench
コマンドは、複数のベンチマークを並行して実行しようとする可能性がありました。しかし、ベンチマークは通常、システムのパフォーマンスを測定するために設計されており、並行実行されると互いに干渉し、正確な測定結果が得られないという問題がありました。例えば、CPUキャッシュの競合、メモリ帯域の飽和、I/Oの競合などが発生し、個々のベンチマークの真の性能特性を把握することが困難になります。
この問題に対処するため、ベンチマークは常に直列(シーケンシャル)に実行されるべきであるという認識が生まれました。これにより、各ベンチマークが独立した環境で実行され、より信頼性の高いパフォーマンスデータを提供できるようになります。
前提知識の解説
go test
コマンドとベンチマーク
Go言語には、標準でテストとベンチマークを実行するための go test
コマンドが用意されています。
- テスト:
func TestXxx(*testing.T)
の形式で記述され、コードの正確性を検証します。 - ベンチマーク:
func BenchmarkXxx(*testing.B)
の形式で記述され、コードのパフォーマンスを測定します。go test -bench .
のように-bench
フラグを指定して実行します。
cmd/go
の内部構造とアクション (action
)
cmd/go
は、Go言語のビルド、テスト、その他の操作を管理する主要なコマンドラインツールです。その内部では、様々なタスク(パッケージのビルド、テストの実行、結果の出力など)が「アクション (action)」として抽象化され、依存関係グラフとして管理されています。
action
構造体:cmd/go
の内部で定義される構造体で、特定のタスク(ビルド、テスト実行、出力など)を表します。deps
フィールド:action
構造体内のフィールドで、そのアクションが実行される前に完了していなければならない他のアクションのリスト(依存関係)を保持します。これにより、タスクの実行順序が制御されます。
go test
コマンドが実行される際、各テストパッケージに対してビルド、テスト実行、結果出力といった一連のアクションが生成されます。これらのアクション間には依存関係が設定され、例えばテスト実行アクションはビルドアクションが完了するまで待機するといった制御が行われます。
技術的詳細
このコミットの核心は、src/cmd/go/test.go
内の runTest
関数におけるベンチマーク実行時の依存関係の変更です。
変更前は、ベンチマーク実行時 (testBench
が true の場合)、各テストのビルドが前のテストの実行完了に依存するように設定されていました。これは、ビルドと実行を完全に直列化しようとする試みでしたが、ベンチマークの並行実行を完全に防ぐものではありませんでした。コメントには「Could instead allow all the builds to run before any benchmarks start, but try this for now.」とあり、この時点ではまだ試行錯誤の段階であったことが伺えます。
変更後は、ベンチマークの実行順序をより厳密に制御するために、依存関係のロジックが修正されました。
- 最初のベンチマーク実行: 最初のベンチマーク実行アクション (
runs[0]
) は、全てのビルドアクション (builds...
) が完了するまで待機するように変更されました。これにより、全てのテストバイナリが準備できてからベンチマークが開始されることが保証されます。 - 後続のベンチマーク実行: 2番目以降のベンチマーク実行アクション (
runs[i]
fori > 0
) は、直前のベンチマークの出力アクション (prints[i-1]
) が完了するまで待機するように変更されました。これは非常に重要な変更点です。ベンチマークの「実行」アクションではなく、「出力」アクションに依存させることで、前のベンチマークが完全に終了し、その結果が出力されてから次のベンチマークが開始されることが保証されます。これにより、ベンチマーク間の干渉が最小限に抑えられ、真の直列実行が実現されます。
また、builder.test
関数内の notest
アクションの依存関係も修正されました。notest
アクションは、テストファイルが存在しないパッケージに対して生成されるアクションで、以前はビルドアクションに依存していましたが、変更後は実行アクション (run
) に依存するように変更されました。これは、テストがない場合でも、実行フェーズが完了してから「テストなし」の出力が行われるようにするためと考えられます。
コアとなるコードの変更箇所
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -446,16 +446,15 @@ func runTest(cmd *Command, args []string) {
}
}
- // If we are benchmarking, force everything to
- // happen in serial. Could instead allow all the
- // builds to run before any benchmarks start,\n-\t// but try this for now.
+ // Force benchmarks to run in serial.
if testBench {
- for i, a := range builds {
- if i > 0 {
- // Make build of test i depend on
- // completing the run of test i-1.
- a.deps = append(a.deps, runs[i-1])
+ // The first run must wait for all builds.
+ // Later runs must wait for the previous run's print.
+ for i, run := range runs {
+ if i == 0 {
+ run.deps = append(run.deps, builds...)
+ } else {
+ run.deps = append(run.deps, prints[i-1])
}
}
}
@@ -516,8 +515,8 @@ func contains(x []string, s string) bool {
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error) {
if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
build := &action{p: p}
- run := &action{p: p}
- print := &action{f: (*builder).notest, p: p, deps: []*action{build}}\n-\t\treturn build, run, print, nil
+ run := &action{p: p, deps: []*action{build}}
+ print := &action{f: (*builder).notest, p: p, deps: []*action{run}}
return build, run, print, nil
}
コアとなるコードの解説
runTest
関数内の変更
if testBench {
- for i, a := range builds {
- if i > 0 {
- // Make build of test i depend on
- // completing the run of test i-1.
- a.deps = append(a.deps, runs[i-1])
+ // The first run must wait for all builds.
+ // Later runs must wait for the previous run's print.
+ for i, run := range runs {
+ if i == 0 {
+ run.deps = append(run.deps, builds...)
+ } else {
+ run.deps = append(run.deps, prints[i-1])
}
}
}
- 変更前:
builds
スライスをイテレートし、i > 0
の場合にbuilds[i]
の依存関係にruns[i-1]
(前のテストの実行アクション) を追加していました。これは、ビルドと実行を交互に直列化しようとするものでしたが、ベンチマークの並行実行を完全に防ぐには不十分でした。 - 変更後:
runs
スライス(ベンチマーク実行アクションのリスト)をイテレートします。i == 0
(最初のベンチマーク実行) の場合、run.deps
にbuilds...
(全てのビルドアクション) を追加します。これにより、全てのベンチマークバイナリがビルドされてから最初のベンチマークが開始されます。i > 0
(2番目以降のベンチマーク実行) の場合、run.deps
にprints[i-1]
(前のベンチマークの出力アクション) を追加します。これにより、前のベンチマークが完全に終了し、その結果が出力されてから次のベンチマークが開始されることが保証され、厳密な直列実行が実現されます。
builder.test
関数内の変更
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error) {
if len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
build := &action{p: p}
- run := &action{p: p}
- print := &action{f: (*builder).notest, p: p, deps: []*action{build}}
+ run := &action{p: p, deps: []*action{build}}
+ print := &action{f: (*builder).notest, p: p, deps: []*action{run}}
return build, run, print, nil
}
- この部分は、テストファイルが存在しないパッケージ (
len(p.TestGoFiles)+len(p.XTestGoFiles) == 0
) の場合の処理です。 - 変更前:
print
アクション(notest
関数を実行する)がbuild
アクションに依存していました。 - 変更後:
print
アクションがrun
アクションに依存するように変更されました。これは、テストがない場合でも、run
アクションが完了(この場合は何も実行しないが、そのフェーズが終了)してから「テストなし」の出力が行われるようにするためです。これにより、アクションの依存関係の整合性が保たれます。
これらの変更により、go test -bench
コマンドは、複数のベンチマークを並行して実行するのではなく、厳密に直列に実行するようになり、より信頼性の高いベンチマーク結果が得られるようになりました。
関連リンク
- Go Issue 5662: https://github.com/golang/go/issues/5662
- Gerrit Change-ID:
https://golang.org/cl/13650043
(これはGerritの古いURL形式ですが、現在はGitHubにリダイレクトされるか、対応するコミットにリンクされています)
参考にした情報源リンク
- Go Issue Tracker: https://github.com/golang/go/issues
- Go Command Documentation: https://pkg.go.dev/cmd/go
- Go Testing Package Documentation: https://pkg.go.dev/testing
- Go Source Code (cmd/go/test.go): https://github.com/golang/go/blob/master/src/cmd/go/test.go