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

[インデックス 18617] ファイルの概要

このコミットは、Go言語の標準ライブラリfmtパッケージのベンチマークテストに関する変更です。具体的には、ベンチマークの実行方法を従来のカスタム並列実行ロジックから、Goのtestingパッケージが提供するb.RunParallel関数を使用するように変更しています。これにより、ベンチマークの信頼性と効率が向上します。

コミット

commit 44cc8e5cc968348f418b57bcf42c692274b1c06c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 24 20:46:25 2014 +0400

    fmt: use RunParallel in benchmarks
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/67910046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/44cc8e5cc968348f418b57bcf42c692274b1c06c

元コミット内容

fmt: use RunParallel in benchmarks

このコミットは、fmtパッケージのベンチマークにおいて、カスタムで実装されていた並列実行ロジックを、Goの標準testingパッケージが提供するRunParallel関数に置き換えることを目的としています。

変更の背景

Goのベンチマークは、testingパッケージによって提供される機能です。初期のGoのベンチマークでは、並列実行をテストするために開発者が手動でゴルーチンを起動し、チャネルやsync/atomicパッケージなどを用いて同期を取る必要がありました。しかし、このようなカスタム実装は複雑であり、正確なベンチマーク結果を得るためには細心の注意が必要でした。特に、CPUコアの利用効率や、ベンチマーク対象のコードが並列実行環境でどのように振る舞うかを正確に測定することは困難でした。

testingパッケージにRunParallel関数が導入されたことで、Goのベンチマークはより簡単に、かつ正確に並列性能を測定できるようになりました。RunParallelは、GOMAXPROCSの値に基づいて自動的にゴルーチンを起動し、各ゴルーチンがb.N回(ベンチマークのイテレーション数)の処理を並列に実行するように調整します。これにより、開発者は並列実行のロジックを自分で書く手間が省け、より信頼性の高いベンチマーク結果を得られるようになりました。

このコミットは、fmtパッケージのベンチマークが、この新しいRunParallelの仕組みに移行することで、ベンチマークコードの簡素化と、より正確な並列性能測定を実現することを目的としています。

前提知識の解説

  • Go言語のベンチマーク: Go言語では、testingパッケージを使用してベンチマークテストを記述できます。関数名のプレフィックスをBenchmarkとすることで、go test -bench=.コマンドで実行可能です。ベンチマーク関数は*testing.B型の引数を取り、b.Nというフィールドを通じて実行回数を制御します。
  • testing.B.RunParallel: RunParallelは、Go 1.2で導入されたtestingパッケージのメソッドです。このメソッドは、ベンチマーク対象のコードを複数のゴルーチンで並列に実行するためのフレームワークを提供します。RunParallelは、GOMAXPROCSの値に基づいて適切な数のゴルーチンを起動し、各ゴルーチンはpb *testing.PB型の引数を受け取る関数を実行します。このpb.Next()メソッドがtrueを返す間、ベンチマーク対象の処理を繰り返し実行します。これにより、ベンチマーク対象のコードが並列環境でどのようにスケールするかを測定できます。
  • GOMAXPROCS: Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。RunParallelはこの値に基づいて並列実行のゴルーチン数を調整します。
  • sync/atomicパッケージ: アトミック操作を提供するパッケージです。複数のゴルーチンから共有変数に安全にアクセスするために使用されます。このコミットの変更前は、カスタムの並列ベンチマークロジックでatomic.AddInt32が使用されていました。
  • bytes.Buffer: 可変長のバイトシーケンスを扱うためのバッファです。fmtパッケージのベンチマークでは、フォーマットされた文字列の書き込み先として使用されることがあります。

技術的詳細

このコミットの主要な変更点は、fmt_test.goファイル内のbenchmarkSprintf関数が削除され、その機能が各ベンチマーク関数(BenchmarkSprintfEmpty, BenchmarkSprintfStringなど)に直接b.RunParallelの呼び出しとして組み込まれたことです。

変更前は、benchmarkSprintf関数がカスタムの並列実行ロジックを持っていました。このロジックは以下の要素を含んでいました。

  1. procs := runtime.GOMAXPROCS(-1): 現在のGOMAXPROCSの値を取得し、並列実行するゴルーチンの数を決定していました。
  2. N := int32(b.N / CallsPerSched): ベンチマークの総イテレーション数b.NCallsPerSchedで割って、各ゴルーチンが処理すべき「チャンク」の数を計算していました。
  3. c := make(chan bool, procs): ゴルーチンの完了を待つためのチャネルを作成していました。
  4. for p := 0; p < procs; p++ { go func() { ... } }: GOMAXPROCSの数だけゴルーチンを起動していました。
  5. for atomic.AddInt32(&N, -1) >= 0 { ... }: 各ゴルーチンはatomic.AddInt32を使って共有カウンタNをデクリメントし、まだ処理すべきチャンクがあるかどうかを判断していました。
  6. for g := 0; g < CallsPerSched; g++ { f(&buf) }: 各チャンク内で、ベンチマーク対象の関数fCallsPerSched回実行していました。

このカスタムロジックは、並列実行をシミュレートするためのものでしたが、RunParallelが提供するより洗練された、Goランタイムに最適化された並列実行メカニズムに比べると、オーバーヘッドや正確性の点で劣る可能性がありました。

変更後は、各ベンチマーク関数が直接b.RunParallelを呼び出すようになりました。

b.RunParallel(func(pb *testing.PB) {
    for pb.Next() {
        // ベンチマーク対象のコード
    }
})

b.RunParallelは、内部でGOMAXPROCSを考慮し、適切な数のワーカーゴルーチンを起動します。各ワーカーゴルーチンはpb.Next()trueを返す間、ループ内でベンチマーク対象のコードを実行します。pb.Next()は、b.Nで指定された総イテレーション数をワーカーゴルーチン間で効率的に分配し、すべてのイテレーションが完了するまでtrueを返します。これにより、手動でのゴルーチン管理、チャネル同期、アトミック操作が不要になり、ベンチマークコードが大幅に簡素化され、かつより正確な並列性能測定が可能になります。

また、import "sync/atomic"が不要になったため、削除されています。

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

src/pkg/fmt/fmt_test.goファイルが変更されています。

具体的には、以下の変更が行われました。

  1. import "sync/atomic" の削除。
  2. benchmarkSprintf 関数の削除。
  3. BenchmarkSprintfEmptyBenchmarkSprintfStringBenchmarkSprintfIntBenchmarkSprintfIntIntBenchmarkSprintfPrefixedIntBenchmarkSprintfFloatBenchmarkManyArgs の各ベンチマーク関数内で、カスタムの並列実行ロジック(benchmarkSprintfの呼び出し)が、b.RunParallelの呼び出しに置き換えられました。

変更前:

func BenchmarkSprintfEmpty(b *testing.B) {
	benchmarkSprintf(b, func(buf *bytes.Buffer) {
		Sprintf("")
	})
}

// ... 他のベンチマーク関数も同様

func benchmarkSprintf(b *testing.B, f func(buf *bytes.Buffer)) {
	const CallsPerSched = 1000
	procs := runtime.GOMAXPROCS(-1)
	N := int32(b.N / CallsPerSched)
	c := make(chan bool, procs)
	for p := 0; p < procs; p++ {
		go func() {
			var buf bytes.Buffer
			for atomic.AddInt32(&N, -1) >= 0 {
				for g := 0; g < CallsPerSched; g++ {
					f(&buf)
				}
			}
			c <- true
		}()
	}
	for p := 0; p < procs; p++ {
		<-c
	}
}

変更後:

func BenchmarkSprintfEmpty(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Sprintf("")
		}
	})
}

// ... 他のベンチマーク関数も同様

func BenchmarkManyArgs(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		var buf bytes.Buffer // 各ゴルーチンで独立したバッファを持つ
		for pb.Next() {
			buf.Reset()
			Fprintf(&buf, "%2d/%2d/%2d %d:%d:%d %s %s\n", 3, 4, 5, 11, 12, 13, "hello", "world")
		}
	})
}

コアとなるコードの解説

このコミットの核心は、Goのベンチマークにおける並列実行のベストプラクティスへの移行です。

変更前は、benchmarkSprintf関数が手動でゴルーチンを管理し、sync/atomicパッケージを使って共有カウンタをアトミックに更新することで、ベンチマークのイテレーションを複数のゴルーチンに分配していました。このアプローチは、並列実行の基本的な概念を実装していましたが、以下のような課題がありました。

  • 複雑性: ゴルーチンの起動、チャネルによる同期、アトミック操作といった低レベルな並列処理のプリミティブを直接扱う必要があり、コードが複雑になりがちでした。
  • 正確性: CallsPerSchedのようなマジックナンバーの調整が必要であり、GOMAXPROCSとの連携も手動で行う必要がありました。これにより、ベンチマークがCPUリソースを最適に利用しているか、あるいは過剰なコンテキストスイッチが発生していないかなどを正確に保証することが困難でした。
  • オーバーヘッド: 手動でのゴルーチン管理や同期メカニズム自体が、ベンチマーク結果に影響を与える可能性のあるオーバーヘッドを生み出す可能性がありました。

b.RunParallelへの移行により、これらの課題が解決されます。

  • 簡素化: 開発者はb.RunParallelを呼び出し、その中にベンチマーク対象のコードをfor pb.Next() { ... }ループで記述するだけでよくなります。並列実行の複雑なロジックはtestingパッケージが内部で処理してくれます。
  • 最適化: RunParallelはGoランタイムと密接に連携しており、GOMAXPROCSの値に基づいて最適な数のゴルーチンを起動し、ベンチマークのイテレーションを効率的に分配します。これにより、CPUリソースの利用が最大化され、より正確で信頼性の高い並列性能の測定が可能になります。
  • 独立性: RunParallelのコールバック関数内で宣言された変数(例: BenchmarkManyArgs内のvar buf bytes.Buffer)は、各ゴルーチンに独立して割り当てられます。これにより、ゴルーチン間の競合状態(race condition)を避けることができ、ベンチマークの信頼性が向上します。

この変更は、Goのベンチマークフレームワークが成熟し、より使いやすく、より正確な並列性能測定を可能にする方向へ進化していることを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のベンチマークに関する一般的な知識
  • testing.B.RunParallelの動作に関する情報
  • sync/atomicパッケージに関する情報
  • bytes.Bufferに関する情報
  • Goのベンチマークの書き方に関するブログ記事やチュートリアル (一般的な知識として参照)
  • Go CL 67910046: https://golang.org/cl/67910046 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
  • GitHubのgolang/goリポジトリ