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

[インデックス 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.WaitGroupatomic パッケージを用いて手動で並行処理を制御することが一般的でした。しかし、このような手動での並行処理の管理は、コードが複雑になりがちで、特に 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 関数では、以下のような手動での並行処理が実装されていました。

  1. b.StopTimer() でタイマーを一時停止し、セットアップ処理(クライアントの初期化など)の時間を計測から除外していました。
  2. runtime.GOMAXPROCS(-1) を呼び出して、現在の GOMAXPROCS の値を取得していました。この値は、起動するゴルーチンの数を決定するために使用されていました。
  3. N := int32(b.N)b.N の値をアトミック操作可能な int32 型の変数にコピーしていました。
  4. sync.WaitGroup を使用して、全てのゴルーチンの完了を待機していました。
  5. procs の数だけゴルーチンを起動し、各ゴルーチン内で for atomic.AddInt32(&N, -1) >= 0 ループを使用して、b.N のイテレーションをアトミックにカウントダウンしながらRPC呼び出しを実行していました。
  6. 各ゴルーチンは、割り当てられたイテレーションが完了すると wg.Done() を呼び出し、メインゴルーチンは wg.Wait() で全てのゴルーチンの完了を待っていました。
  7. b.StartTimer() でベンチマークの計測を再開していました。

このアプローチは機能的には正しいものの、以下のような課題がありました。

  • 複雑性: GOMAXPROCS の取得、WaitGroup の管理、アトミックカウンタによるイテレーションの分配など、並行処理の低レベルな詳細を開発者が手動で管理する必要がありました。
  • エラーの可能性: 手動での同期プリミティブの利用は、デッドロックや競合状態などのバグを導入するリスクがありました。
  • 柔軟性の欠如: GOMAXPROCS の値に直接依存するため、テスト環境や実行環境の変化に対して柔軟に対応しにくい側面がありました。

変更後の実装 (b.RunParallel の利用)

変更後の benchmarkEndToEnd 関数では、b.RunParallel メソッドが導入されました。

  1. b.StopTimer() の呼び出しが削除され、代わりに b.ResetTimer() が呼び出されています。b.ResetTimer() は、タイマーをリセットし、それまでの計測時間を破棄して、すぐに計測を開始します。これにより、セットアップ処理の時間を計測から除外するという目的は達成されつつ、より簡潔な記述になっています。
  2. b.RunParallel(func(pb *testing.PB) { ... }) が呼び出されています。
    • RunParallel は、GOMAXPROCS の値に基づいて適切な数のゴルーチンを自動的に起動します。
    • 各ゴルーチンは、引数として渡された匿名関数を実行します。
    • 匿名関数内では、for pb.Next() { ... } ループが使用されています。pb.Next() は、b.N のイテレーションを各ゴルーチンに自動的に分配し、まだ処理すべきイテレーションがある場合に true を返します。これにより、手動でのアトミックカウンタや WaitGroup の管理が不要になります。
    • 各ゴルーチンは、pb.Next()false を返すまでRPC呼び出しを繰り返し実行します。
  3. benchmarkEndToEndAsync 関数でも同様に、b.StopTimer() が削除され、b.ResetTimer() が追加されています。ただし、benchmarkEndToEndAsync は元々 RunParallel を使用していなかったため、このコミットでは RunParallel への移行は行われていません。これは、benchmarkEndToEndAsync が非同期RPC呼び出しのベンチマークであり、MaxConcurrentCalls のような独自の並行性制御ロジックを持っているため、RunParallel の直接的な適用が適切ではないと判断された可能性があります。

b.RunParallel の利点

  • 簡潔性: 並行処理のロジックが RunParallel メソッド内にカプセル化されるため、ベンチマークコードが大幅に簡潔になります。
  • 正確性: RunParallel は、b.N のイテレーションを複数のゴルーチンに均等かつ効率的に分配します。これにより、ベンチマーク結果の信頼性と再現性が向上します。
  • 自動スケーリング: RunParallelGOMAXPROCS の値に基づいてゴルーチンの数を自動的に調整するため、異なる環境でのベンチマーク実行時にも最適な並行度で実行されます。
  • 慣用的な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.WaitGroupwg.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 メソッドに移行することで、コードの簡潔性、正確性、および保守性を大幅に向上させています。

関連リンク

参考にした情報源リンク