[インデックス 11074] ファイルの概要
このコミットは、Go言語のコマンドラインツールであるgo build
およびgo test
に、並列処理を制御するための-p
フラグを追加するものです。これにより、複数のパッケージのビルドやテストを並行して実行できるようになり、特にマルチコアCPU環境での処理速度が大幅に向上します。
コミット
- コミットハッシュ:
8d8829c6718d571d0155753c6ef0c1118c903826
- 作者: Russ Cox rsc@golang.org
- コミット日時: 2012年1月9日 月曜日 21:06:31 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8d8829c6718d571d0155753c6ef0c1118c903826
元コミット内容
cmd/go: add -p flag for parallelism (like make -j)
On my MacBookAir4,1:
19.94r go install -a -p 1 std
12.36r go install -a -p 2 std
9.76r go install -a -p 3 std
10.77r go install -a -p 4 std
86.57r go test -p 1 std -short
52.69r go test -p 2 std -short
43.75r go test -p 3 std -short
40.44r go test -p 4 std -short
157.50r go test -p 1 std
99.58r go test -p 2 std
87.24r go test -p 3 std
80.18r go test -p 4 std
R=golang-dev, adg, r
CC=golang-dev
https://golang.org/cl/5531057
変更の背景
Go言語の初期のビルドおよびテストシステムは、複数のパッケージを処理する際に基本的に逐次実行(一つずつ順番に実行)されていました。これは、特に多数のパッケージを持つ大規模なプロジェクトや、マルチコアプロセッサを搭載した現代のコンピュータ環境において、ビルドやテストの完了に時間がかかるという問題を引き起こしていました。
このコミットの目的は、このパフォーマンスのボトルネックを解消することにあります。make
コマンドの-j
フラグ(ジョブ数を指定して並列実行を可能にする)と同様の機能を持つ-p
フラグをgo build
およびgo test
コマンドに導入することで、利用可能なCPUコアを最大限に活用し、ビルドおよびテストプロセスを並列化することが可能になります。
コミットメッセージに示されているベンチマーク結果は、この変更がもたらす顕著なパフォーマンス改善を明確に示しています。例えば、go install -a std
の実行時間が-p 1
(逐次実行)の場合の19.94秒から、-p 3
の場合の9.76秒へと半減しており、並列処理の有効性が実証されています。
前提知識の解説
Go言語 (Golang)
Googleによって開発されたオープンソースのプログラミング言語です。静的型付け、コンパイル型、ガベージコレクションを備え、特に並行処理に強みを持っています。
go build
コマンド
Goのソースコードをコンパイルして実行可能ファイルやパッケージを生成するコマンドです。通常、依存関係にあるパッケージも自動的にビルドします。
go test
コマンド
Goのパッケージに含まれるテストを実行するコマンドです。テストコードは通常、_test.go
というサフィックスを持つファイルに記述されます。
並列処理 (Parallelism)
複数のタスクを同時に実行する計算手法です。マルチコアプロセッサの恩恵を最大限に受けるために重要です。Go言語は、ゴルーチン(goroutine)とチャネル(channel)という軽量な並行処理の仕組みを言語レベルでサポートしています。
make -j
Unix系のビルドツールであるmake
コマンドのオプションで、-j N
と指定することで、最大N個のジョブ(ビルドタスク)を並列に実行するよう指示します。このコミットの-p
フラグは、このmake -j
の概念をGoのビルド・テストシステムに持ち込むものです。
runtime.NumCPU()
Go言語の標準ライブラリruntime
パッケージに含まれる関数で、現在のシステムで利用可能な論理CPUコアの数を返します。-p
フラグのデフォルト値として、この関数の戻り値が使用されます。
sync.WaitGroup
Go言語のsync
パッケージに含まれる型で、複数のゴルーチンの完了を待機するために使用されます。このコミットでは、並列実行されるビルド/テストタスクの完了を管理するために利用されています。
flag
パッケージ
Go言語でコマンドライン引数を解析するための標準パッケージです。このコミットでは、-p
フラグの定義と解析にこのパッケージが使用されています。
技術的詳細
このコミットの主要な変更点は、Goコマンドのビルドおよびテストロジックに並列実行のメカニズムを導入したことです。
-
-p
フラグの導入:go build
、go install
、go test
コマンドに-p N
フラグが追加されました。N
は並列実行するジョブの最大数を指定します。- デフォルト値は
runtime.NumCPU()
によって取得されるシステム上の論理CPUコア数に設定されます。 -n
フラグ(コマンドの実行はせず、表示のみを行う)が指定された場合は、並列数を1に制限し、出力の決定論的順序を保証します。
-
builder
構造体の変更:- ビルドプロセスを管理する
builder
構造体から、個々のフラグ(aflag
,nflag
,vflag
,xflag
)が削除され、代わりにグローバル変数(buildA
,buildN
,buildV
,buildX
,buildP
)が使用されるようになりました。これにより、フラグの管理が一元化され、複数のコマンド間で共有しやすくなっています。 builder.init()
メソッドは、これらのグローバル変数に依存するように変更されました。
- ビルドプロセスを管理する
-
並列実行の制御 (
builder.do
メソッド):builder.do
メソッドは、ビルド/テストの依存関係グラフを走査し、実行可能なアクションを特定します。- このメソッド内で、
-p
フラグで指定された数(par
変数)のゴルーチンが起動されます。 - これらのゴルーチンは、
b.readySema
というチャネル(セマフォとして機能)から値を受け取ることで、実行可能なアクションを取得し、処理します。 - 各アクションの完了後、
b.doneSema
チャネルに値を送信し、メインのゴルーチンがすべてのタスクの完了を待機します。
-
テスト実行の並列化と出力管理 (
test.go
):go test
コマンドのロジックが大幅に修正されました。b.test
関数は、ビルドアクション、実行アクションに加えて、新しくprintAction
を返すようになりました。runTest
アクションは、テストの標準出力と標準エラー出力をbytes.Buffer
にバッファリングするようになりました。これにより、並列実行されたテストの出力が混ざり合うのを防ぎます。printTest
アクションが導入され、runTest
が完了した後にバッファリングされたテスト結果を標準出力に書き出す役割を担います。これにより、テスト結果の表示順序が保証されます。- ベンチマーク実行時(
-bench
フラグが指定された場合)は、すべてのビルドが完了してからベンチマークが逐次実行されるように、依存関係が調整されます。これは、ベンチマークの安定性と比較可能性を確保するためです。 - テスト結果の出力に実行時間(
%.3fs
形式)が含まれるようになりました。
-
フラグ定義の共通化:
addBuildFlags
というヘルパー関数が導入され、go build
とgo install
コマンドで共通のフラグ(-a
,-n
,-p
,-v
,-x
)を簡単に登録できるようになりました。
これらの変更により、Goのビルドおよびテストシステムは、現代のマルチコア環境でより効率的に動作するようになりました。
コアとなるコードの変更箇所
このコミットでは、主に以下の4つのファイルが変更されています。
src/cmd/go/build.go
:go build
およびgo install
コマンドのビルドロジックとフラグ定義。src/cmd/go/run.go
:go run
コマンドのフラグ定義。src/cmd/go/test.go
:go test
コマンドのテスト実行ロジックとフラグ定義。src/cmd/go/testflag.go
:go test
コマンドのフラグ解析ロジック。
src/cmd/go/build.go
の主な変更点
cmdBuild
とcmdInstall
のUsageLine
に[-p n]
が追加。buildA
,buildN
,buildV
,buildX
などのフラグ変数がグローバル変数として定義され、runtime.NumCPU()
で初期化されるbuildP
が追加。addBuildFlags
関数が追加され、共通フラグの登録をカプセル化。builder.init
メソッドの引数から個々のフラグが削除され、グローバル変数を使用するように変更。builder.do
メソッド内で、並列実行数を制御するpar
変数が導入され、for i := 0; i < par; i++
ループでゴルーチンが起動されるように変更。b.nflag
などのbuilder
構造体のフィールド参照が、buildN
などのグローバル変数参照に置き換え。
src/cmd/go/run.go
の主な変更点
cmdRun
のUsageLine
に[-p n]
は追加されていないが、init
関数内でbuildA
,buildN
,buildX
といったグローバルフラグ変数を共有するように変更。
src/cmd/go/test.go
の主な変更点
cmdTest
のUsageLine
に[-p n]
が追加。testP
という新しいフラグ変数が追加。runTest
関数内で、buildX = testX
とbuildP = testP
が設定され、テストコマンドのフラグがビルドシステムに伝達されるように変更。b.test
関数が、buildAction
,runAction
に加えてprintAction
を返すように変更。runTest
メソッド内で、テストの標準出力がa.testOutput
というbytes.Buffer
にバッファリングされるように変更。printTest
という新しいメソッドが追加され、バッファリングされたテスト結果を最終的に出力する役割を担う。- ベンチマーク実行時(
testBench
がtrueの場合)の依存関係が調整され、逐次実行が強制されるように変更。 - テスト結果の出力に実行時間(例:
ok \tpackage \t0.123s
)が含まれるように変更。
src/cmd/go/testflag.go
の主な変更点
usageMessage
に-p=n
フラグの説明が追加。testFlagDefn
に-p
フラグの定義が追加。setIntFlag
というヘルパー関数が追加され、整数型のフラグ値を解析。-bench
フラグの処理が追加され、testBench
変数を設定。
コアとなるコードの解説
src/cmd/go/build.go
における並列処理の導入
// Flags set by multiple commands.
var buildA bool // -a flag
var buildN bool // -n flag
var buildP = runtime.NumCPU() // -p flag
var buildV bool // -v flag
var buildX bool // -x flag
// addBuildFlags adds the flags common to the build and install commands.
func addBuildFlags(cmd *Command) {
cmd.Flag.BoolVar(&buildA, "a", false, "")
cmd.Flag.BoolVar(&buildN, "n", false, "")
cmd.Flag.IntVar(&buildP, "p", buildP, "") // -p flagを登録
cmd.Flag.BoolVar(&buildV, "v", false, "")
cmd.Flag.BoolVar(&buildX, "x", false, "")
}
// ...
func (b *builder) do(root *action) {
// ...
// Kick off goroutines according to parallelism.
// If we are using the -n flag (just printing commands)
// drop the parallelism to 1, both to make the output
// deterministic and because there is no real work anyway.
par := buildP // グローバル変数buildPから並列数を取得
if buildN {
par = 1 // -nフラグが指定された場合は並列数を1に制限
}
for i := 0; i < par; i++ { // 指定された並列数分のゴルーチンを起動
go func() {
for _ = range b.readySema { // セマフォから値を受け取り、実行可能なアクションを処理
// ...
}
}()
}
// ...
}
buildP
というグローバル変数が導入され、デフォルトでruntime.NumCPU()
(論理CPUコア数)に初期化されます。addBuildFlags
関数を通じて、go build
やgo install
コマンドに-p
フラグが登録されます。builder.do
メソッドでは、このbuildP
の値に基づいてpar
(並列数)が決定され、その数だけゴルーチンが起動されます。これらのゴルーチンはb.readySema
というチャネルを介してタスクを受け取り、並列に処理を進めます。-n
フラグが指定された場合は、出力の順序性を保つために並列数が1に制限されます。
src/cmd/go/test.go
におけるテストの並列化と出力制御
func runTest(cmd *Command, args []string) {
// ...
buildX = testX
if testP > 0 {
buildP = testP // go testの-pフラグの値をビルドシステムに伝達
}
// ...
var builds, runs, prints []*action // printsアクションが追加
// Prepare build + run + print actions for all packages being tested.
for _, p := range pkgs {
buildTest, runTest, printTest, err := b.test(p) // b.testがprintTestアクションも返すように変更
// ...
prints = append(prints, printTest)
}
// Ultimately the goal is to print the output.
root := &action{deps: prints} // 最終的な依存関係のルートをprintsアクションに設定
// Force the printing of results to happen in order, one at a time.
for i, a := range prints {
if i > 0 {
a.deps = append(a.deps, prints[i-1]) // printsアクションの順序を強制
}
}
// ...
}
func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error) {
// ...
// run test
runAction = &action{
f: (*builder).runTest,
deps: []*action{pmainAction},
p: p,
ignoreFail: true,
}
printAction = &action{ // printActionの定義
f: (*builder).printTest,
deps: []*action{runAction},
p: p,
}
return pmainAction, runAction, printAction, nil
}
func (b *builder) runTest(a *action) error {
// ...
a.testOutput = new(bytes.Buffer) // テスト出力をバッファリング
// ...
out, err := cmd.CombinedOutput() // コマンドの出力を取得
// ...
fmt.Fprintf(a.testOutput, "ok \t%s\t%s\n", a.p.ImportPath, t) // バッファに書き込み
// ...
return nil
}
// printTest is the action for printing a test result.
func (b *builder) printTest(a *action) error {
run := a.deps[0]
os.Stdout.Write(run.testOutput.Bytes()) // バッファリングされた出力を標準出力に書き出す
run.testOutput = nil
return nil
}
go test
コマンドでは、testP
フラグの値がbuildP
に設定され、ビルドシステム全体で並列数が共有されます。
最も重要な変更は、テストの実行と出力の分離です。b.test
関数は、テストのビルドと実行に加えて、その結果を出力するためのprintAction
を返すようになりました。runTest
メソッドは、テストの実行結果を直接標準出力に書き出すのではなく、a.testOutput
というbytes.Buffer
にバッファリングします。そして、printTest
メソッドが、このバッファリングされた内容を適切なタイミング(通常は逐次的に)標準出力に書き出します。これにより、複数のテストが並列に実行されても、その出力が混ざり合うことなく、期待される順序で表示されるようになります。また、テストの実行時間も出力に含まれるようになりました。
関連リンク
- Go Change-Id:
5531057
(Gerrit Code Review): https://golang.org/cl/5531057 - Go言語公式ドキュメント: https://go.dev/doc/
go build
コマンドのドキュメント: https://go.dev/cmd/go/#hdr-Compile_packages_and_dependenciesgo test
コマンドのドキュメント: https://go.dev/cmd/go/#hdr-Test_packagessync
パッケージのドキュメント: https://pkg.go.dev/syncruntime
パッケージのドキュメント: https://pkg.go.dev/runtime
参考にした情報源リンク
- GNU Make Manual (Parallel Execution): https://www.gnu.org/software/make/manual/html_node/Parallel-Execution.html
- Go Concurrency Patterns: https://go.dev/blog/concurrency-patterns
- Go
flag
package: https://pkg.go.dev/flag