Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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コマンドのビルドおよびテストロジックに並列実行のメカニズムを導入したことです。

  1. -pフラグの導入:

    • go buildgo installgo testコマンドに-p Nフラグが追加されました。Nは並列実行するジョブの最大数を指定します。
    • デフォルト値はruntime.NumCPU()によって取得されるシステム上の論理CPUコア数に設定されます。
    • -nフラグ(コマンドの実行はせず、表示のみを行う)が指定された場合は、並列数を1に制限し、出力の決定論的順序を保証します。
  2. builder構造体の変更:

    • ビルドプロセスを管理するbuilder構造体から、個々のフラグ(aflag, nflag, vflag, xflag)が削除され、代わりにグローバル変数(buildA, buildN, buildV, buildX, buildP)が使用されるようになりました。これにより、フラグの管理が一元化され、複数のコマンド間で共有しやすくなっています。
    • builder.init()メソッドは、これらのグローバル変数に依存するように変更されました。
  3. 並列実行の制御 (builder.doメソッド):

    • builder.doメソッドは、ビルド/テストの依存関係グラフを走査し、実行可能なアクションを特定します。
    • このメソッド内で、-pフラグで指定された数(par変数)のゴルーチンが起動されます。
    • これらのゴルーチンは、b.readySemaというチャネル(セマフォとして機能)から値を受け取ることで、実行可能なアクションを取得し、処理します。
    • 各アクションの完了後、b.doneSemaチャネルに値を送信し、メインのゴルーチンがすべてのタスクの完了を待機します。
  4. テスト実行の並列化と出力管理 (test.go):

    • go testコマンドのロジックが大幅に修正されました。
    • b.test関数は、ビルドアクション、実行アクションに加えて、新しくprintActionを返すようになりました。
    • runTestアクションは、テストの標準出力と標準エラー出力をbytes.Bufferにバッファリングするようになりました。これにより、並列実行されたテストの出力が混ざり合うのを防ぎます。
    • printTestアクションが導入され、runTestが完了した後にバッファリングされたテスト結果を標準出力に書き出す役割を担います。これにより、テスト結果の表示順序が保証されます。
    • ベンチマーク実行時(-benchフラグが指定された場合)は、すべてのビルドが完了してからベンチマークが逐次実行されるように、依存関係が調整されます。これは、ベンチマークの安定性と比較可能性を確保するためです。
    • テスト結果の出力に実行時間(%.3fs形式)が含まれるようになりました。
  5. フラグ定義の共通化:

    • addBuildFlagsというヘルパー関数が導入され、go buildgo installコマンドで共通のフラグ(-a, -n, -p, -v, -x)を簡単に登録できるようになりました。

これらの変更により、Goのビルドおよびテストシステムは、現代のマルチコア環境でより効率的に動作するようになりました。

コアとなるコードの変更箇所

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/cmd/go/build.go: go buildおよびgo installコマンドのビルドロジックとフラグ定義。
  2. src/cmd/go/run.go: go runコマンドのフラグ定義。
  3. src/cmd/go/test.go: go testコマンドのテスト実行ロジックとフラグ定義。
  4. src/cmd/go/testflag.go: go testコマンドのフラグ解析ロジック。

src/cmd/go/build.go の主な変更点

  • cmdBuildcmdInstallUsageLine[-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 の主な変更点

  • cmdRunUsageLine[-p n]は追加されていないが、init関数内でbuildA, buildN, buildXといったグローバルフラグ変数を共有するように変更。

src/cmd/go/test.go の主な変更点

  • cmdTestUsageLine[-p n]が追加。
  • testPという新しいフラグ変数が追加。
  • runTest関数内で、buildX = testXbuildP = 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 buildgo 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メソッドが、このバッファリングされた内容を適切なタイミング(通常は逐次的に)標準出力に書き出します。これにより、複数のテストが並列に実行されても、その出力が混ざり合うことなく、期待される順序で表示されるようになります。また、テストの実行時間も出力に含まれるようになりました。

関連リンク

参考にした情報源リンク