[インデックス 18216] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmtパッケージのテストファイルであるsrc/pkg/fmt/fmt_test.goに対する変更です。このファイルは、fmtパッケージの各種フォーマット関数(例: Sprintf, Fprintf)の機能テストとパフォーマンスベンチマークを含んでいます。今回の変更は、既存のベンチマークを並列実行可能にするためのものです。
コミット
このコミットは、fmtパッケージのベンチマークを並列化することを目的としています。これは、sync.Poolの変更をベンチマークするのに最適なターゲットであると判断されたためです。具体的には、Sprintfなどのフォーマット操作が内部的に一時的なバッファやオブジェクトを頻繁に利用する可能性があり、sync.Poolのようなオブジェクトプーリングメカニズムの性能評価には、並行処理下での挙動を測定することが不可欠であるため、ベンチマークの並列化が実施されました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f62d087771d44b9670e2f34a0d3cef73c01a020
元コミット内容
fmt: make benchmarks parallel
This seems to be the best target to benchmark sync.Pool changes.
This is resend of cl/49910043 which was LGTMed by
TBR=bradfitz
R=golang-codereviews
CC=golang-codereviews
https://golang.org/cl/50140045
変更の背景
この変更の主な背景には、Go言語のsync.Poolパッケージの導入と、その性能を正確に評価する必要性がありました。
-
sync.Poolの性能評価:sync.Poolは、一時的に使用されるオブジェクトの再利用を促進し、ガベージコレクションの負荷を軽減することで、アプリケーションのパフォーマンスを向上させるためのメカニズムです。fmtパッケージのSprintfやFprintfのような関数は、内部で文字列操作やバッファリングのために一時的なオブジェクト(例:bytes.Buffer)を頻繁に生成・破棄する可能性があります。このような処理においてsync.Poolが効果的に機能するかどうかを評価するには、単一のスレッドでの実行だけでなく、複数のゴルーチンが並行してこれらの操作を行うシナリオでの性能を測定することが不可欠です。 -
並行処理の重要性: Go言語は並行処理を強力にサポートしており、多くのGoアプリケーションは複数のゴルーチンを同時に実行します。
sync.Poolのような並行処理に関連する最適化の真価は、並行負荷の下で発揮されます。そのため、ベンチマークも並行処理をシミュレートするように設計されるべきでした。 -
既存ベンチマークの限界: 従来のGoのベンチマーク(
for i := 0; i < b.N; i++)は、基本的に単一のゴルーチンで実行されるため、並行処理のオーバーヘッドや競合状態による影響を正確に測定できませんでした。このコミットは、この限界を克服し、より現実的なシナリオでのパフォーマンス測定を可能にすることを目指しました。
前提知識の解説
Goのベンチマーク (testingパッケージ)
Go言語には、標準ライブラリのtestingパッケージにベンチマーク機能が組み込まれています。
go test -bench=.: このコマンドでベンチマークを実行します。func BenchmarkXxx(b *testing.B): ベンチマーク関数はBenchmarkで始まり、*testing.B型の引数を取ります。b.N: ベンチマーク関数内でループを回す回数を示します。testingパッケージが自動的に適切なb.Nの値を決定し、統計的に有意な結果が得られるように調整します。b.ResetTimer(): ベンチマーク対象のコードの実行時間を測定するタイマーをリセットします。セットアップコードの時間を測定から除外するために使用されます。b.StopTimer()/b.StartTimer(): タイマーの一時停止と再開。b.RunParallel(func(pb *testing.PB)): Go 1.7で導入された、ベンチマークを並列実行するための標準的なAPIです。このコミットの時点(2014年)ではまだ存在していなかったため、カスタムの並列化ロジックが実装されています。
sync.Pool
sync.Poolは、Go言語の標準ライブラリsyncパッケージに含まれる型で、一時的に使用されるオブジェクトの再利用を可能にするためのオブジェクトプールを提供します。
- 目的: オブジェクトの生成とガベージコレクションのコストを削減すること。特に、短期間だけ使用され、頻繁に生成・破棄されるオブジェクト(例:
bytes.Buffer、[]byteスライス)に有効です。 - 仕組み:
Get()メソッドでプールからオブジェクトを取得し、Put()メソッドでオブジェクトをプールに戻します。プールが空の場合、Newフィールドに設定された関数が呼び出されて新しいオブジェクトが生成されます。 - 注意点:
sync.Poolに格納されたオブジェクトは、ガベージコレクションの際にいつでも削除される可能性があります。そのため、プールに格納されるオブジェクトは、その状態が失われても問題ない、または簡単に再初期化できるものである必要があります。
Goの並行処理の基本
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドです。
goキーワードを使って関数呼び出しの前に置くことで、新しいゴルーチンを起動できます。 - チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goにおける並行処理の主要な同期プリミティブです。
runtime.GOMAXPROCS: Goプログラムが同時に実行できるOSスレッドの最大数を設定します。runtime.GOMAXPROCS(-1)は、現在のGOMAXPROCSの値を変更せずに取得するために使用されます。デフォルトでは、Go 1.5以降は論理CPUの数に設定されます。sync/atomicパッケージ: 複数のゴルーチンから共有される変数に対して、アトミック(不可分)な操作を提供します。これにより、競合状態(race condition)を防ぎ、データの整合性を保つことができます。atomic.AddInt32は、int32型の変数に値をアトミックに加算します。
bytes.Buffer
bytes.Bufferは、bytesパッケージに含まれる可変長バイトバッファです。
- 用途: 効率的なバイト列の構築や操作に使用されます。特に、
fmt.Fprintfのように出力先としてio.Writerインターフェースを実装するオブジェクトが必要な場合に便利です。 Reset()メソッド: バッファの内容をクリアし、再利用可能にします。これにより、新しいバッファを毎回割り当てるオーバーヘッドを避けることができます。
技術的詳細
このコミットの技術的な核心は、Goのベンチマークフレームワークが提供するb.RunParallelがまだ存在しなかった時期に、カスタムで並列ベンチマークを実行するメカニズムを実装した点にあります。
変更前は、各ベンチマーク関数は以下のような単純なループ構造を持っていました。
func BenchmarkSprintfEmpty(b *testing.B) {
for i := 0; i < b.N; i++ {
Sprintf("")
}
}
これは単一のゴルーチンでb.N回操作を実行するもので、並行処理の性能特性を測定するには不十分でした。
変更後、すべてのBenchmarkSprintf*関数は、新しく導入されたヘルパー関数benchmarkSprintfを呼び出すように変更されました。
func BenchmarkSprintfEmpty(b *testing.B) {
benchmarkSprintf(b, func(buf *bytes.Buffer) {
Sprintf("")
})
}
そして、benchmarkSprintf関数が並列実行のロジックをカプセル化しています。
-
並列度 (Concurrency):
procs := runtime.GOMAXPROCS(-1)runtime.GOMAXPROCS(-1)を呼び出すことで、Goランタイムが利用可能な論理CPUの数を取得し、これを並列実行するゴルーチンの数としています。これにより、システムのリソースを最大限に活用した並列ベンチマークが可能になります。 -
作業の分散:
const CallsPerSched = 1000 N := int32(b.N / CallsPerSched)b.N(ベンチマークの総実行回数)をCallsPerSched(各ゴルーチンが一度に処理する呼び出し回数)で割ることで、各ゴルーチンが処理すべき「バッチ」の数を計算しています。Nはアトミックカウンタとして機能し、すべてのゴルーチンが協力してb.N回の操作を完了するように調整されます。 -
ゴルーチンの起動と実行:
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 // 完了を通知 }() }procsの数だけゴルーチンを起動します。- 各ゴルーチンは、
bytes.Bufferのインスタンスをローカルに持ちます。これは、Fprintfのような関数がbytes.Bufferを使用する場合に、ゴルーチン間で競合状態が発生しないようにするためです。もしbytes.Bufferが共有されていた場合、並行書き込みによってデータ破損や不正な結果が生じる可能性があります。 for atomic.AddInt32(&N, -1) >= 0ループは、Nが0になるまで(つまり、b.N回の操作がすべて完了するまで)、各ゴルーチンがCallsPerSched回ずつベンチマーク関数fを実行し続けることを保証します。atomic.AddInt32を使用することで、複数のゴルーチンが同時にNを更新しても、カウンタの整合性が保たれます。- 各ゴルーチンは、自身の作業が完了すると、チャネル
cにtrueを送信して完了を通知します。
-
ゴルーチンの完了待機:
for p := 0; p < procs; p++ { <-c // すべてのゴルーチンの完了を待機 }メインのベンチマーク関数は、
procsの数だけチャネルcから値を受信することで、すべての並列ゴルーチンがその作業を完了するのを待ちます。これにより、ベンチマークの実行が完全に終了したことを確認できます。
このカスタム実装は、b.RunParallelが導入される前のGoのベンチマークにおいて、並行処理の性能を測定するための効果的な手段でした。特にsync.Poolのような並行処理に特化した最適化の評価には、このような並列ベンチマークが不可欠です。
コアとなるコードの変更箇所
src/pkg/fmt/fmt_test.go ファイルが変更されました。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -11,6 +11,7 @@ import (
"math"
"runtime"
"strings"
+ "sync/atomic" // <-- 追加
"testing"
"time"
"unicode"
@@ -606,46 +607,66 @@ func TestReorder(t *testing.T) {
}
func BenchmarkSprintfEmpty(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("")
- }
+ })
}
func BenchmarkSprintfString(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%s", "hello")
- }
+ })
}
func BenchmarkSprintfInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%d", 5)
- }
+ })
}
func BenchmarkSprintfIntInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%d %d", 5, 6)
- }
+ })
}
func BenchmarkSprintfPrefixedInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("This is some meaningless prefix text that needs to be scanned %d", 6)
- }
+ })
}
func BenchmarkSprintfFloat(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%g", 5.23184)
- }
+ })
}
func BenchmarkManyArgs(b *testing.B) {
- var buf bytes.Buffer
- for i := 0; i < b.N; i++ {\
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
buf.Reset()
- Fprintf(&buf, "%2d/%2d/%2d %d:%d:%d %s %s\n", 3, 4, 5, 11, 12, 13, "hello", "world")
- }
+ Fprintf(buf, "%2d/%2d/%2d %d:%d:%d %s %s\n", 3, 4, 5, 11, 12, 13, "hello", "world")
+ })
+}
+
+// <-- 新しいヘルパー関数 benchmarkSprintf の追加
+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
+ }
}
コアとなるコードの解説
このコミットの主要な変更は、benchmarkSprintfという新しいヘルパー関数の導入と、既存のBenchmarkSprintf*関数群がこのヘルパー関数を利用するように変更された点です。
-
import "sync/atomic"の追加: 並列処理において共有されるカウンタNを安全に操作するために、sync/atomicパッケージがインポートされました。これにより、複数のゴルーチンが同時にNを更新する際の競合状態を防ぎます。 -
BenchmarkSprintf*関数の変更: 以前はfor i := 0; i < b.N; i++というループでベンチマーク対象の関数を直接呼び出していましたが、変更後はbenchmarkSprintf(b, func(buf *bytes.Buffer) { ... })という形式になりました。これにより、実際のベンチマークロジック(例:Sprintf(""))は匿名関数としてbenchmarkSprintfに渡され、benchmarkSprintfがその匿名関数を並列に実行する責任を負います。Fprintfを使用するBenchmarkManyArgsでは、bytes.Bufferを引数として匿名関数に渡すことで、各ゴルーチンが独立したバッファを使用できるようにしています。 -
benchmarkSprintf関数の追加と詳細: この関数が並列ベンチマークの核心です。const CallsPerSched = 1000: 各ゴルーチンが一度に実行するベンチマーク操作の回数を定義しています。これは、ゴルーチン間の同期オーバーヘッドを減らしつつ、作業を効率的に分散するための調整可能な定数です。procs := runtime.GOMAXPROCS(-1): システムの論理CPU数を取得し、それと同じ数のゴルーチンを起動して並列処理を行います。これにより、利用可能なCPUリソースを最大限に活用できます。N := int32(b.N / CallsPerSched): ベンチマークの総実行回数b.NをCallsPerSchedで割ることで、各ゴルーチンが処理すべき「バッチ」の総数を計算し、Nに格納します。このNは、後述のアトミック操作によって複数のゴルーチン間で共有され、全体の進捗を管理します。c := make(chan bool, procs): バッファ付きチャネルを作成します。このチャネルは、起動された各ゴルーチンが自身の作業を完了したことをメインのゴルーチンに通知するために使用されます。バッファサイズをprocsにすることで、ゴルーチンがチャネルに送信する際にブロックされるのを防ぎます。for p := 0; p < procs; p++ { go func() { ... }():procsの数だけ匿名関数をゴルーチンとして起動します。var buf bytes.Buffer: 各ゴルーチンは、bytes.Bufferの独自のインスタンスを持ちます。これは、Fprintfのような操作がbytes.Bufferに書き込む際に、ゴルーチン間で競合が発生しないようにするための重要な設計です。各ゴルーチンが独立したバッファを持つことで、並行処理の安全性が確保されます。for atomic.AddInt32(&N, -1) >= 0: このループが、b.N回の操作をすべてのゴルーチンで分担して実行するための中心的なロジックです。atomic.AddInt32(&N, -1): 共有カウンタNの値をアトミックに1減らします。これにより、複数のゴルーチンが同時にこの操作を行っても、Nの値が正しく更新されることが保証されます。>= 0:Nが0以上である限りループを続けます。つまり、まだ処理すべき「バッチ」が残っている限り、ゴルーチンは作業を続行します。
for g := 0; g < CallsPerSched; g++ { f(&buf) }: 各ゴルーチンは、Nが0になるまで、CallsPerSched回ずつ、渡されたベンチマーク関数fを実行します。c <- true: ゴルーチンが自身の担当するすべての作業を完了したら、チャネルcにtrueを送信して、メインのゴルーチンに完了を通知します。
for p := 0; p < procs; p++ { <-c }: メインのゴルーチンは、procsの数だけチャネルcから値を受信することで、起動したすべてのゴルーチンが完了するのを待ちます。これにより、ベンチマークの測定がすべての並列処理が終了した後に正確に行われることが保証されます。
この実装は、Go 1.7で導入されたb.RunParallelの機能に先駆けて、カスタムで並列ベンチマークを実現したものであり、当時のGoの並行処理の理解と活用を示す良い例です。
関連リンク
- Go言語
testingパッケージのドキュメント: https://pkg.go.dev/testing - Go言語
syncパッケージのドキュメント (特にsync.Pool): https://pkg.go.dev/sync - Go言語
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語
runtimeパッケージのドキュメント (特にGOMAXPROCS): https://pkg.go.dev/runtime - Go言語
bytesパッケージのドキュメント (特にbytes.Buffer): https://pkg.go.dev/bytes
参考にした情報源リンク
- Go言語の公式ドキュメント (上記「関連リンク」に記載)
- Goのベンチマークに関する一般的な記事やチュートリアル (例: Goの公式ブログ、Goに関する技術ブログなど)
sync.Poolの利用方法や設計思想に関する記事 (例: Goの公式ブログ、Goに関する技術ブログなど)- Goの並行処理に関する一般的な知識 (ゴルーチン、チャネル、アトミック操作など)
- GitHubのgolang/goリポジトリのコミット履歴と関連するコードレビュー (CL)
[インデックス 18216] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmtパッケージのテストファイルであるsrc/pkg/fmt/fmt_test.goに対する変更です。このファイルは、fmtパッケージの各種フォーマット関数(例: Sprintf, Fprintf)の機能テストとパフォーマンスベンチマークを含んでいます。今回の変更は、既存のベンチマークを並列実行可能にするためのものです。
コミット
このコミットは、fmtパッケージのベンチマークを並列化することを目的としています。これは、sync.Poolの変更をベンチマークするのに最適なターゲットであると判断されたためです。具体的には、Sprintfなどのフォーマット操作が内部的に一時的なバッファやオブジェクトを頻繁に利用する可能性があり、sync.Poolのようなオブジェクトプーリングメカニズムの性能評価には、並行処理下での挙動を測定することが不可欠であるため、ベンチマークの並列化が実施されました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7f62d087771d44b9670e2f34a0d3cef73c01a020
元コミット内容
fmt: make benchmarks parallel
This seems to be the best target to benchmark sync.Pool changes.
This is resend of cl/49910043 which was LGTMed by
TBR=bradfitz
R=golang-codereviews
CC=golang-codereviews
https://golang.org/cl/50140045
変更の背景
この変更の主な背景には、Go言語のsync.Poolパッケージの導入と、その性能を正確に評価する必要性がありました。
-
sync.Poolの性能評価:sync.Poolは、一時的に使用されるオブジェクトの再利用を促進し、ガベージコレクションの負荷を軽減することで、アプリケーションのパフォーマンスを向上させるためのメカニズムです。fmtパッケージのSprintfやFprintfのような関数は、内部で文字列操作やバッファリングのために一時的なオブジェクト(例:bytes.Buffer)を頻繁に生成・破棄する可能性があります。このような処理においてsync.Poolが効果的に機能するかどうかを評価するには、単一のスレッドでの実行だけでなく、複数のゴルーチンが並行してこれらの操作を行うシナリオでの性能を測定することが不可欠です。 -
並行処理の重要性: Go言語は並行処理を強力にサポートしており、多くのGoアプリケーションは複数のゴルーチンを同時に実行します。
sync.Poolのような並行処理に関連する最適化の真価は、並行負荷の下で発揮されます。そのため、ベンチマークも並行処理をシミュレートするように設計されるべきでした。 -
既存ベンチマークの限界: 従来のGoのベンチマーク(
for i := 0; i < b.N; i++)は、基本的に単一のゴルーチンで実行されるため、並行処理のオーバーヘッドや競合状態による影響を正確に測定できませんでした。このコミットは、この限界を克服し、より現実的なシナリオでのパフォーマンス測定を可能にすることを目指しました。
前提知識の解説
Goのベンチマーク (testingパッケージ)
Go言語には、標準ライブラリのtestingパッケージにベンチマーク機能が組み込まれています。
go test -bench=.: このコマンドでベンチマークを実行します。func BenchmarkXxx(b *testing.B): ベンチマーク関数はBenchmarkで始まり、*testing.B型の引数を取ります。b.N: ベンチマーク関数内でループを回す回数を示します。testingパッケージが自動的に適切なb.Nの値を決定し、統計的に有意な結果が得られるように調整します。b.ResetTimer(): ベンチマーク対象のコードの実行時間を測定するタイマーをリセットします。セットアップコードの時間を測定から除外するために使用されます。b.StopTimer()/b.StartTimer(): タイマーの一時停止と再開。b.RunParallel(func(pb *testing.PB)): Go 1.7で導入された、ベンチマークを並列実行するための標準的なAPIです。このコミットの時点(2014年)ではまだ存在していなかったため、カスタムの並列化ロジックが実装されています。
sync.Pool
sync.Poolは、Go言語の標準ライブラリsyncパッケージに含まれる型で、一時的に使用されるオブジェクトの再利用を可能にするためのオブジェクトプールを提供します。
- 目的: オブジェクトの生成とガベージコレクションのコストを削減すること。特に、短期間だけ使用され、頻繁に生成・破棄されるオブジェクト(例:
bytes.Buffer、[]byteスライス)に有効です。 - 仕組み:
Get()メソッドでプールからオブジェクトを取得し、Put()メソッドでオブジェクトをプールに戻します。プールが空の場合、Newフィールドに設定された関数が呼び出されて新しいオブジェクトが生成されます。 - 注意点:
sync.Poolに格納されたオブジェクトは、ガベージコレクションの際にいつでも削除される可能性があります。そのため、プールに格納されるオブジェクトは、その状態が失われても問題ない、または簡単に再初期化できるものである必要があります。
Goの並行処理の基本
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドです。
goキーワードを使って関数呼び出しの前に置くことで、新しいゴルーチンを起動できます。 - チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goにおける並行処理の主要な同期プリミティブです。
runtime.GOMAXPROCS: Goプログラムが同時に実行できるOSスレッドの最大数を設定します。runtime.GOMAXPROCS(-1)は、現在のGOMAXPROCSの値を変更せずに取得するために使用されます。デフォルトでは、Go 1.5以降は論理CPUの数に設定されます。sync/atomicパッケージ: 複数のゴルーチンから共有される変数に対して、アトミック(不可分)な操作を提供します。これにより、競合状態(race condition)を防ぎ、データの整合性を保つことができます。atomic.AddInt32は、int32型の変数に値をアトミックに加算します。
bytes.Buffer
bytes.Bufferは、bytesパッケージに含まれる可変長バイトバッファです。
- 用途: 効率的なバイト列の構築や操作に使用されます。特に、
fmt.Fprintfのように出力先としてio.Writerインターフェースを実装するオブジェクトが必要な場合に便利です。 Reset()メソッド: バッファの内容をクリアし、再利用可能にします。これにより、新しいバッファを毎回割り当てるオーバーヘッドを避けることができます。
技術的詳細
このコミットの技術的な核心は、Goのベンチマークフレームワークが提供するb.RunParallelがまだ存在しなかった時期に、カスタムで並列ベンチマークを実行するメカニズムを実装した点にあります。
変更前は、各ベンチマーク関数は以下のような単純なループ構造を持っていました。
func BenchmarkSprintfEmpty(b *testing.B) {
for i := 0; i < b.N; i++ {
Sprintf("")
}
}
これは単一のゴルーチンでb.N回操作を実行するもので、並行処理の性能特性を測定するには不十分でした。
変更後、すべてのBenchmarkSprintf*関数は、新しく導入されたヘルパー関数benchmarkSprintfを呼び出すように変更されました。
func BenchmarkSprintfEmpty(b *testing.B) {
benchmarkSprintf(b, func(buf *bytes.Buffer) {
Sprintf("")
})
}
そして、benchmarkSprintf関数が並列実行のロジックをカプセル化しています。
-
並列度 (Concurrency):
procs := runtime.GOMAXPROCS(-1)runtime.GOMAXPROCS(-1)を呼び出すことで、Goランタイムが利用可能な論理CPUの数を取得し、これを並列実行するゴルーチンの数としています。これにより、システムのリソースを最大限に活用した並列ベンチマークが可能になります。 -
作業の分散:
const CallsPerSched = 1000 N := int32(b.N / CallsPerSched)b.N(ベンチマークの総実行回数)をCallsPerSched(各ゴルーチンが一度に処理する呼び出し回数)で割ることで、各ゴルーチンが処理すべき「バッチ」の数を計算しています。Nはアトミックカウンタとして機能し、すべてのゴルーチンが協力してb.N回の操作を完了するように調整されます。 -
ゴルーチンの起動と実行:
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 // 完了を通知 }() }procsの数だけゴルーチンを起動します。- 各ゴルーチンは、
bytes.Bufferのインスタンスをローカルに持ちます。これは、Fprintfのような関数がbytes.Bufferを使用する場合に、ゴルーチン間で競合状態が発生しないようにするためです。もしbytes.Bufferが共有されていた場合、並行書き込みによってデータ破損や不正な結果が生じる可能性があります。 for atomic.AddInt32(&N, -1) >= 0ループは、Nが0になるまで(つまり、b.N回の操作がすべて完了するまで)、各ゴルーチンがCallsPerSched回ずつベンチマーク関数fを実行し続けることを保証します。atomic.AddInt32を使用することで、複数のゴルーチンが同時にNを更新しても、カウンタの整合性が保たれます。- 各ゴルーチンは、自身の作業が完了すると、チャネル
cにtrueを送信して完了を通知します。
-
ゴルーチンの完了待機:
for p := 0; p < procs; p++ { <-c // すべてのゴルーチンの完了を待機 }メインのベンチマーク関数は、
procsの数だけチャネルcから値を受信することで、すべての並列ゴルーチンがその作業を完了するのを待ちます。これにより、ベンチマークの実行が完全に終了したことを確認できます。
このカスタム実装は、b.RunParallelが導入される前のGoのベンチマークにおいて、並行処理の性能を測定するための効果的な手段でした。特にsync.Poolのような並行処理に特化した最適化の評価には、このような並列ベンチマークが不可欠です。
コアとなるコードの変更箇所
src/pkg/fmt/fmt_test.go ファイルが変更されました。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -11,6 +11,7 @@ import (
"math"
"runtime"
"strings"
+ "sync/atomic" // <-- 追加
"testing"
"time"
"unicode"
@@ -606,46 +607,66 @@ func TestReorder(t *testing.T) {
}
func BenchmarkSprintfEmpty(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("")
- }
+ })
}
func BenchmarkSprintfString(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%s", "hello")
- }
+ })
}
func BenchmarkSprintfInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%d", 5)
- }
+ })
}
func BenchmarkSprintfIntInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%d %d", 5, 6)
- }
+ })
}
func BenchmarkSprintfPrefixedInt(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("This is some meaningless prefix text that needs to be scanned %d", 6)
- }
+ })
}
func BenchmarkSprintfFloat(b *testing.B) {
- for i := 0; i < b.N; i++ {
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
Sprintf("%g", 5.23184)
- }
+ })
}
func BenchmarkManyArgs(b *testing.B) {
- var buf bytes.Buffer
- for i := 0; i < b.N; i++ {\
+ benchmarkSprintf(b, func(buf *bytes.Buffer) { // <-- 変更
buf.Reset()
- Fprintf(&buf, "%2d/%2d/%2d %d:%d:%d %s %s\n", 3, 4, 5, 11, 12, 13, "hello", "world")
- }
+ Fprintf(buf, "%2d/%2d/%2d %d:%d:%d %s %s\n", 3, 4, 5, 11, 12, 13, "hello", "world")
+ })
+}
+
+// <-- 新しいヘルパー関数 benchmarkSprintf の追加
+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
+ }
}
コアとなるコードの解説
このコミットの主要な変更は、benchmarkSprintfという新しいヘルパー関数の導入と、既存のBenchmarkSprintf*関数群がこのヘルパー関数を利用するように変更された点です。
-
import "sync/atomic"の追加: 並列処理において共有されるカウンタNを安全に操作するために、sync/atomicパッケージがインポートされました。これにより、複数のゴルーチンが同時にNを更新する際の競合状態を防ぎます。 -
BenchmarkSprintf*関数の変更: 以前はfor i := 0; i < b.N; i++というループでベンチマーク対象の関数を直接呼び出していましたが、変更後はbenchmarkSprintf(b, func(buf *bytes.Buffer) { ... })という形式になりました。これにより、実際のベンチマークロジック(例:Sprintf(""))は匿名関数としてbenchmarkSprintfに渡され、benchmarkSprintfがその匿名関数を並列に実行する責任を負います。Fprintfを使用するBenchmarkManyArgsでは、bytes.Bufferを引数として匿名関数に渡すことで、各ゴルーチンが独立したバッファを使用できるようにしています。 -
benchmarkSprintf関数の追加と詳細: この関数が並列ベンチマークの核心です。const CallsPerSched = 1000: 各ゴルーチンが一度に実行するベンチマーク操作の回数を定義しています。これは、ゴルーチン間の同期オーバーヘッドを減らしつつ、作業を効率的に分散するための調整可能な定数です。procs := runtime.GOMAXPROCS(-1): システムの論理CPU数を取得し、それと同じ数のゴルーチンを起動して並列処理を行います。これにより、利用可能なCPUリソースを最大限に活用できます。N := int32(b.N / CallsPerSched): ベンチマークの総実行回数b.NをCallsPerSchedで割ることで、各ゴルーチンが処理すべき「バッチ」の総数を計算し、Nに格納します。このNは、後述のアトミック操作によって複数のゴルーチン間で共有され、全体の進捗を管理します。c := make(chan bool, procs): バッファ付きチャネルを作成します。このチャネルは、起動された各ゴルーチンが自身の作業を完了したことをメインのゴルーチンに通知するために使用されます。バッファサイズをprocsにすることで、ゴルーチンがチャネルに送信する際にブロックされるのを防ぎます。for p := 0; p < procs; p++ { go func() { ... }():procsの数だけ匿名関数をゴルーチンとして起動します。var buf bytes.Buffer: 各ゴルーチンは、bytes.Bufferの独自のインスタンスを持ちます。これは、Fprintfのような操作がbytes.Bufferに書き込む際に、ゴルーチン間で競合が発生しないようにするための重要な設計です。各ゴルーチンが独立したバッファを持つことで、並行処理の安全性が確保されます。for atomic.AddInt32(&N, -1) >= 0: このループが、b.N回の操作をすべてのゴルーチンで分担して実行するための中心的なロジックです。atomic.AddInt32(&N, -1): 共有カウンタNの値をアトミックに1減らします。これにより、複数のゴルーチンが同時にこの操作を行っても、Nの値が正しく更新されることが保証されます。>= 0:Nが0以上である限りループを続けます。つまり、まだ処理すべき「バッチ」が残っている限り、ゴルーチンは作業を続行します。
for g := 0; g < CallsPerSched; g++ { f(&buf) }: 各ゴルーチンは、Nが0になるまで、CallsPerSched回ずつ、渡されたベンチマーク関数fを実行します。c <- true: ゴルーチンが自身の担当するすべての作業を完了したら、チャネルcにtrueを送信して、メインのゴルーチンに完了を通知します。
for p := 0; p < procs; p++ { <-c }: メインのゴルーチンは、procsの数だけチャネルcから値を受信することで、起動したすべてのゴルーチンが完了するのを待ちます。これにより、ベンチマークの測定がすべての並列処理が終了した後に正確に行われることが保証されます。
この実装は、Go 1.7で導入されたb.RunParallelの機能に先駆けて、カスタムで並列ベンチマークを実現したものであり、当時のGoの並行処理の理解と活用を示す良い例です。
関連リンク
- Go言語
testingパッケージのドキュメント: https://pkg.go.dev/testing - Go言語
syncパッケージのドキュメント (特にsync.Pool): https://pkg.go.dev/sync - Go言語
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語
runtimeパッケージのドキュメント (特にGOMAXPROCS): https://pkg.go.dev/runtime - Go言語
bytesパッケージのドキュメント (特にbytes.Buffer): https://pkg.go.dev/bytes
参考にした情報源リンク
- Go言語の公式ドキュメント (上記「関連リンク」に記載)
- Goのベンチマークに関する一般的な記事やチュートリアル (例: Goの公式ブログ、Goに関する技術ブログなど)
sync.Poolの利用方法や設計思想に関する記事 (例: Goの公式ブログ、Goに関する技術ブログなど)- Goの並行処理に関する一般的な知識 (ゴルーチン、チャネル、アトミック操作など)
- GitHubのgolang/goリポジトリのコミット履歴と関連するコードレビュー (CL)