[インデックス 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)