[インデックス 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
関数がカスタムの並列実行ロジックを持っていました。このロジックは以下の要素を含んでいました。
procs := runtime.GOMAXPROCS(-1)
: 現在のGOMAXPROCS
の値を取得し、並列実行するゴルーチンの数を決定していました。N := int32(b.N / CallsPerSched)
: ベンチマークの総イテレーション数b.N
をCallsPerSched
で割って、各ゴルーチンが処理すべき「チャンク」の数を計算していました。c := make(chan bool, procs)
: ゴルーチンの完了を待つためのチャネルを作成していました。for p := 0; p < procs; p++ { go func() { ... } }
:GOMAXPROCS
の数だけゴルーチンを起動していました。for atomic.AddInt32(&N, -1) >= 0 { ... }
: 各ゴルーチンはatomic.AddInt32
を使って共有カウンタN
をデクリメントし、まだ処理すべきチャンクがあるかどうかを判断していました。for g := 0; g < CallsPerSched; g++ { f(&buf) }
: 各チャンク内で、ベンチマーク対象の関数f
をCallsPerSched
回実行していました。
このカスタムロジックは、並列実行をシミュレートするためのものでしたが、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
ファイルが変更されています。
具体的には、以下の変更が行われました。
import "sync/atomic"
の削除。benchmarkSprintf
関数の削除。BenchmarkSprintfEmpty
、BenchmarkSprintfString
、BenchmarkSprintfInt
、BenchmarkSprintfIntInt
、BenchmarkSprintfPrefixedInt
、BenchmarkSprintfFloat
、BenchmarkManyArgs
の各ベンチマーク関数内で、カスタムの並列実行ロジック(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 testingパッケージのドキュメント: https://pkg.go.dev/testing
- Go 1.2リリースノート (RunParallelの導入について): https://go.dev/doc/go1.2#testing
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のベンチマークに関する一般的な知識
testing.B.RunParallel
の動作に関する情報sync/atomic
パッケージに関する情報bytes.Buffer
に関する情報- Goのベンチマークの書き方に関するブログ記事やチュートリアル (一般的な知識として参照)
- Go CL 67910046: https://golang.org/cl/67910046 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
- GitHubのgolang/goリポジトリ