[インデックス 18613] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/rpc
パッケージのベンチマークテストにおいて、並行処理の記述方法を testing.B
型の RunParallel
メソッドを使用するように変更するものです。これにより、ベンチマークの記述がより簡潔になり、Goのテストフレームワークが提供する並行処理の恩恵を最大限に活用できるようになります。
コミット
commit b5705ed9ab26d69cb32d484289efc5f1cbc50144
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 24 20:23:35 2014 +0400
net/rpc: use RunParallel in benchmarks
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/68040044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b5705ed9ab26d69cb32d484289efc5f1cbc50144
元コミット内容
net/rpc
パッケージのベンチマークにおいて、並行処理を手動で実装していた部分を testing.B.RunParallel
を用いるように変更しました。
変更の背景
Go言語の testing
パッケージは、ベンチマークテストを記述するための強力な機能を提供しています。初期のベンチマークでは、複数のゴルーチンを起動し、sync.WaitGroup
や atomic
パッケージを用いて手動で並行処理を制御することが一般的でした。しかし、このような手動での並行処理の管理は、コードが複雑になりがちで、特に b.N
(ベンチマークのイテレーション回数) の分配や、タイマーの開始・停止のタイミングを正確に制御するのが困難でした。
testing.B.RunParallel
メソッドは、Go 1.1で導入された機能で、ベンチマークテストにおける並行処理をより簡単かつ効率的に記述できるように設計されています。このメソッドを使用することで、テストフレームワークが自動的に GOMAXPROCS
の値に基づいて適切な数のゴルーチンを起動し、b.N
のイテレーションをそれらのゴルーチンに均等に分散させます。これにより、ベンチマークの信頼性と再現性が向上し、開発者は並行処理の低レベルな詳細に煩わされることなく、ベンチマーク対象のコードロジックに集中できるようになります。
このコミットは、net/rpc
パッケージのベンチマークコードを、この新しい慣用的な RunParallel
の使用に移行することで、ベンチマークの品質と保守性を向上させることを目的としています。
前提知識の解説
Go言語のベンチマークテスト (testing
パッケージ)
Go言語には、標準ライブラリとして testing
パッケージが提供されており、ユニットテスト、例、そしてベンチマークテストを記述するための機能が含まれています。
go test
コマンド: Goのテストを実行するためのコマンドです。-bench
フラグを使用することでベンチマークテストを実行できます。例:go test -bench=.
*testing.B
型: ベンチマーク関数はfunc BenchmarkXxx(b *testing.B)
の形式で定義され、*testing.B
型の引数を受け取ります。この型はベンチマークの実行を制御するための様々なメソッドを提供します。b.N
: ベンチマーク関数内でループの回数を指定するために使用される整数です。go test
コマンドがベンチマークを実行する際に、このb.N
の値は動的に調整され、ベンチマーク対象の操作が十分な回数実行されるようにします。これにより、統計的に有意な結果が得られるようになります。b.StopTimer()
/b.StartTimer()
: ベンチマークの計測時間を一時停止/再開するために使用します。ベンチマークのセットアップやクリーンアップなど、計測対象ではない処理の時間を計測から除外するために利用されます。b.ResetTimer()
:b.StartTimer()
と似ていますが、タイマーをリセットし、それまでの計測時間を破棄します。通常、セットアップ処理の後に呼び出され、純粋なベンチマーク対象の処理時間のみを計測するために使われます。runtime.GOMAXPROCS
: Goプログラムが同時に実行できるOSスレッドの最大数を制御する環境変数、または関数です。この値は、GoランタイムがゴルーチンをOSスレッドにマッピングする際に影響を与えます。ベンチマークにおいて、この値は並行処理の度合いを決定する重要な要素となります。sync.WaitGroup
: 複数のゴルーチンの完了を待つための同期プリミティブです。Add
で待つゴルーチンの数を増やし、各ゴルーチンが完了時にDone
を呼び出し、Wait
で全てのゴルーチンが完了するまでブロックします。sync/atomic
パッケージ: アトミック操作(不可分操作)を提供するパッケージです。複数のゴルーチンから共有変数にアクセスする際に、競合状態を防ぎ、正確な値を保証するために使用されます。特にatomic.AddInt32
は、32ビット整数をアトミックに加算するために使われます。b.RunParallel(func(pb *testing.PB))
: Go 1.1で導入された、ベンチマークテストを並行して実行するためのメソッドです。このメソッドは、GOMAXPROCS
の値に基づいて複数のゴルーチンを起動し、各ゴルーチンにb.N
のイテレーションを分散させます。引数として渡される関数は、各並行実行ゴルーチン内で実行され、*testing.PB
型の引数を受け取ります。*testing.PB
型:RunParallel
のコールバック関数に渡される型で、pb.Next()
メソッドを提供します。各ゴルーチンはfor pb.Next() { ... }
ループを使って、割り当てられたイテレーションを処理します。pb.Next()
は、まだ処理すべきイテレーションがある場合にtrue
を返し、次のイテレーションに進みます。これにより、b.N
のイテレーションが複数のゴルーチンに自動的に分配されます。
RPC (Remote Procedure Call)
RPCは、異なるアドレス空間にあるプロセス(通常はネットワーク上の異なるマシン)が、あたかもローカルな手続き呼び出しであるかのように通信できる技術です。net/rpc
パッケージは、Go言語でRPCサービスを簡単に構築するための機能を提供します。
技術的詳細
このコミットの主要な変更点は、net/rpc/server_test.go
内の benchmarkEndToEnd
関数と benchmarkEndToEndAsync
関数におけるベンチマークの並行実行ロジックの置き換えです。
変更前の実装
変更前の benchmarkEndToEnd
関数では、以下のような手動での並行処理が実装されていました。
b.StopTimer()
でタイマーを一時停止し、セットアップ処理(クライアントの初期化など)の時間を計測から除外していました。runtime.GOMAXPROCS(-1)
を呼び出して、現在のGOMAXPROCS
の値を取得していました。この値は、起動するゴルーチンの数を決定するために使用されていました。N := int32(b.N)
でb.N
の値をアトミック操作可能なint32
型の変数にコピーしていました。sync.WaitGroup
を使用して、全てのゴルーチンの完了を待機していました。procs
の数だけゴルーチンを起動し、各ゴルーチン内でfor atomic.AddInt32(&N, -1) >= 0
ループを使用して、b.N
のイテレーションをアトミックにカウントダウンしながらRPC呼び出しを実行していました。- 各ゴルーチンは、割り当てられたイテレーションが完了すると
wg.Done()
を呼び出し、メインゴルーチンはwg.Wait()
で全てのゴルーチンの完了を待っていました。 b.StartTimer()
でベンチマークの計測を再開していました。
このアプローチは機能的には正しいものの、以下のような課題がありました。
- 複雑性:
GOMAXPROCS
の取得、WaitGroup
の管理、アトミックカウンタによるイテレーションの分配など、並行処理の低レベルな詳細を開発者が手動で管理する必要がありました。 - エラーの可能性: 手動での同期プリミティブの利用は、デッドロックや競合状態などのバグを導入するリスクがありました。
- 柔軟性の欠如:
GOMAXPROCS
の値に直接依存するため、テスト環境や実行環境の変化に対して柔軟に対応しにくい側面がありました。
変更後の実装 (b.RunParallel
の利用)
変更後の benchmarkEndToEnd
関数では、b.RunParallel
メソッドが導入されました。
b.StopTimer()
の呼び出しが削除され、代わりにb.ResetTimer()
が呼び出されています。b.ResetTimer()
は、タイマーをリセットし、それまでの計測時間を破棄して、すぐに計測を開始します。これにより、セットアップ処理の時間を計測から除外するという目的は達成されつつ、より簡潔な記述になっています。b.RunParallel(func(pb *testing.PB) { ... })
が呼び出されています。RunParallel
は、GOMAXPROCS
の値に基づいて適切な数のゴルーチンを自動的に起動します。- 各ゴルーチンは、引数として渡された匿名関数を実行します。
- 匿名関数内では、
for pb.Next() { ... }
ループが使用されています。pb.Next()
は、b.N
のイテレーションを各ゴルーチンに自動的に分配し、まだ処理すべきイテレーションがある場合にtrue
を返します。これにより、手動でのアトミックカウンタやWaitGroup
の管理が不要になります。 - 各ゴルーチンは、
pb.Next()
がfalse
を返すまでRPC呼び出しを繰り返し実行します。
benchmarkEndToEndAsync
関数でも同様に、b.StopTimer()
が削除され、b.ResetTimer()
が追加されています。ただし、benchmarkEndToEndAsync
は元々RunParallel
を使用していなかったため、このコミットではRunParallel
への移行は行われていません。これは、benchmarkEndToEndAsync
が非同期RPC呼び出しのベンチマークであり、MaxConcurrentCalls
のような独自の並行性制御ロジックを持っているため、RunParallel
の直接的な適用が適切ではないと判断された可能性があります。
b.RunParallel
の利点
- 簡潔性: 並行処理のロジックが
RunParallel
メソッド内にカプセル化されるため、ベンチマークコードが大幅に簡潔になります。 - 正確性:
RunParallel
は、b.N
のイテレーションを複数のゴルーチンに均等かつ効率的に分配します。これにより、ベンチマーク結果の信頼性と再現性が向上します。 - 自動スケーリング:
RunParallel
はGOMAXPROCS
の値に基づいてゴルーチンの数を自動的に調整するため、異なる環境でのベンチマーク実行時にも最適な並行度で実行されます。 - 慣用的なGo: Goのテストフレームワークが提供する公式の機能を使用することで、コードの可読性と保守性が向上します。
この変更は、Goのベンチマークテストのベストプラクティスに準拠し、net/rpc
パッケージのベンチマークの品質を向上させるものです。
コアとなるコードの変更箇所
src/pkg/net/rpc/server_test.go
ファイルの以下の部分が変更されました。
--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -594,7 +594,6 @@ func TestErrorAfterClientClose(t *testing.T) {
}
func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {
- b.StopTimer()
once.Do(startServer)
client, err := dial()
if err != nil {
@@ -604,33 +603,24 @@ func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {
// Synchronous calls
args := &Args{7, 8}
- procs := runtime.GOMAXPROCS(-1)
- N := int32(b.N)
- var wg sync.WaitGroup
- wg.Add(procs)
- b.StartTimer()
-
- for p := 0; p < procs; p++ {
- go func() {
- reply := new(Reply)
- for atomic.AddInt32(&N, -1) >= 0 {
- err := client.Call("Arith.Add", args, reply)
- if err != nil {
- b.Fatalf("rpc error: Add: expected no error but got string %q", err.Error())
- }
- if reply.C != args.A+args.B {
- b.Fatalf("rpc error: Add: expected %d got %d", reply.C, args.A+args.B)
- }
- }
- wg.Done()
- }()
- }
- wg.Wait()
+ b.ResetTimer()
+
+ b.RunParallel(func(pb *testing.PB) {
+ reply := new(Reply)
+ for pb.Next() {
+ err := client.Call("Arith.Add", args, reply)
+ if err != nil {
+ b.Fatalf("rpc error: Add: expected no error but got string %q", err.Error())
+ }
+ if reply.C != args.A+args.B {
+ b.Fatalf("rpc error: Add: expected %d got %d", reply.C, args.A+args.B)
+ }
+ }
+ })
}
func benchmarkEndToEndAsync(dial func() (*Client, error), b *testing.B) {
const MaxConcurrentCalls = 100
- b.StopTimer()
once.Do(startServer)
client, err := dial()
if err != nil {
@@ -647,7 +637,7 @@ func benchmarkEndToEndAsync(dial func() (*Client, error), b *testing.B) {
wg.Add(procs)
gate := make(chan bool, MaxConcurrentCalls)
res := make(chan *Call, MaxConcurrentCalls)
- b.StartTimer()
+ b.ResetTimer()
for p := 0; p < procs; p++ {
go func() {
コアとなるコードの解説
benchmarkEndToEnd
関数の変更
-
削除されたコード:
b.StopTimer()
: ベンチマークの計測を一時停止する呼び出しが削除されました。procs := runtime.GOMAXPROCS(-1)
:GOMAXPROCS
の値を取得する行が削除されました。RunParallel
が内部でこの値を考慮するため不要になりました。N := int32(b.N)
:b.N
をアトミックカウンタとして使用するための変数宣言が削除されました。var wg sync.WaitGroup
とwg.Add(procs)
、wg.Done()
、wg.Wait()
: 手動でのゴルーチン同期のためのWaitGroup
の利用が削除されました。for p := 0; p < procs; p++ { go func() { ... } }
:procs
の数だけゴルーチンを手動で起動するループが削除されました。for atomic.AddInt32(&N, -1) >= 0
: アトミックカウンタを使ってb.N
のイテレーションを管理するループが削除されました。b.StartTimer()
: ベンチマークの計測を再開する呼び出しが削除されました。
-
追加・変更されたコード:
b.ResetTimer()
:b.StopTimer()
とb.StartTimer()
の代わりに導入されました。これにより、セットアップ処理の時間を計測から除外しつつ、ベンチマークの計測をすぐに開始できます。b.RunParallel(func(pb *testing.PB) { ... })
: この行が、手動で記述されていた並行処理のロジック全体を置き換えています。func(pb *testing.PB)
:RunParallel
に渡される匿名関数です。この関数は、RunParallel
によって起動される各ゴルーチンで実行されます。pb
は*testing.PB
型のインスタンスで、このゴルーチンに割り当てられたイテレーションを管理します。reply := new(Reply)
: 各ゴルーチン内でRPCの応答を格納するためのReply
オブジェクトが新しく作成されます。これにより、ゴルーチン間で共有される状態が減り、競合状態のリスクが低減します。for pb.Next() { ... }
: このループが、各ゴルーチンに割り当てられたb.N
のイテレーションを実行します。pb.Next()
は、まだ処理すべきイテレーションがある限りtrue
を返し、次のイテレーションに進む準備をします。これにより、b.N
のイテレーションがRunParallel
によって自動的に複数のゴルーチンに均等に分配されます。client.Call("Arith.Add", args, reply)
: RPC呼び出し自体は変更されていません。
benchmarkEndToEndAsync
関数の変更
- 削除されたコード:
b.StopTimer()
:benchmarkEndToEnd
と同様に削除されました。
- 追加・変更されたコード:
b.ResetTimer()
:b.StopTimer()
の代わりに導入されました。
このコミットは、Goのベンチマークテストにおける並行処理の記述を、より現代的で慣用的な b.RunParallel
メソッドに移行することで、コードの簡潔性、正確性、および保守性を大幅に向上させています。
関連リンク
- Go言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing testing.B.RunParallel
のドキュメント: https://pkg.go.dev/testing#B.RunParallel- Go 1.1 Release Notes (Benchmarking improvements): https://go.dev/doc/go1.1#benchmarking
参考にした情報源リンク
- 上記のGo言語公式ドキュメント
- Go言語のベンチマークに関する一般的な情報源 (例: Goのブログ記事、チュートリアルなど)
- A quick guide to Go benchmarks: https://go.dev/blog/benchmarking
- Writing benchmarks in Go: https://dave.cheney.net/2013/06/02/writing-benchmarks-in-go
- Go testing package: https://www.ardanlabs.com/blog/2017/02/go-testing-package.html
- Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines (直接的ではないが、Goの並行処理の理解に役立つ)
- Go RPC package: https://pkg.go.dev/net/rpc