[インデックス 18614] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内のベンチマークテスト serve_test.go
において、並行ベンチマークの実行方法を testing.B
の RunParallel
メソッドを使用するように変更するものです。これにより、ベンチマークの記述が簡潔になり、Goのテストフレームワークが提供する並行実行の恩恵を享受できるようになります。
コミット
net/http
パッケージのベンチマークテスト serve_test.go
において、並行ベンチマークのロジックが手動でゴルーチンと sync.WaitGroup
を使って実装されていた箇所を、testing
パッケージが提供する RunParallel
メソッドに置き換えました。これにより、ベンチマークコードが簡素化され、よりGoのテストフレームワークの慣習に沿った形になります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/96d52298189b556f9fabca9a940f60a18fbc49d9
元コミット内容
net/http: use RunParallel in benchmarks
LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/68070043
変更の背景
この変更の背景には、Goのベンチマークテストにおける並行処理の記述をより効率的かつ標準的な方法に統一するという意図があります。以前のコードでは、runtime.GOMAXPROCS
と sync.WaitGroup
を組み合わせて手動で複数のゴルーチンを起動し、ベンチマークのイテレーションを並行して実行していました。しかし、Go 1.1で導入された testing.B.RunParallel
メソッドは、このような並行ベンチマークのシナリオをより簡単に、かつ最適に処理するための専用のAPIを提供します。
RunParallel
を使用することで、開発者は手動でのゴルーチン管理や sync.WaitGroup
の同期処理から解放され、ベンチマークのロジック自体に集中できるようになります。また、RunParallel
は GOMAXPROCS
の値に基づいて最適な並行度を自動的に調整するため、様々な環境でのベンチマークの再現性と信頼性が向上します。このコミットは、net/http
パッケージのベンチマークをGoのテストフレームワークの最新のベストプラクティスに準拠させることを目的としています。
前提知識の解説
Goのベンチマークテスト (testing
パッケージ)
Go言語には、標準ライブラリとして testing
パッケージが提供されており、ユニットテスト、例示テスト、そしてベンチマークテストを記述するための機能が含まれています。
- ベンチマーク関数: ベンチマーク関数は
func BenchmarkXxx(b *testing.B)
というシグネチャを持ちます。*testing.B
型の引数b
は、ベンチマークの実行を制御するためのメソッドを提供します。 b.N
: ベンチマーク関数内で、b.N
はベンチマーク対象の操作を何回繰り返すかを示す数値です。ベンチマーク実行時には、b.N
の値は動的に調整され、操作が安定した時間内に実行されるようにします。b.ReportAllocs()
: メモリ割り当ての統計をレポートに含めるように指示します。b.StopTimer()
/b.StartTimer()
: ベンチマークの計測を一時停止/再開します。セットアップ処理など、ベンチマーク対象ではない処理の時間を計測から除外するために使用されます。b.ResetTimer()
: タイマーをリセットし、b.N
の値もリセットします。通常、セットアップ処理の後に呼び出され、純粋なベンチマーク対象の処理時間のみを計測するために使われます。
testing.B.RunParallel
RunParallel
はGo 1.1で導入された testing.B
のメソッドで、並行してベンチマークを実行するために設計されています。
- 並行実行:
RunParallel
は、GOMAXPROCS
の値に基づいて複数のゴルーチンを起動し、これらのゴルーチン間でb.N
のイテレーションを分散して実行します。これにより、CPUコアを効率的に利用し、並行処理の性能を測定できます。 b.SetParallelism(p int)
:RunParallel
が起動するゴルーチンの数を明示的に設定するために使用できます。デフォルトではGOMAXPROCS
の値が使われます。pb *testing.PB
:RunParallel
に渡される関数は*testing.PB
型の引数を受け取ります。このpb
オブジェクトはNext()
メソッドを提供し、各ゴルーチンが次のイテレーションを実行すべきかどうかを判断するために使用されます。for pb.Next() { ... }
のループ内でベンチマーク対象の操作を記述します。- 利点:
- 簡潔なコード: 手動でのゴルーチン管理や
sync.WaitGroup
の使用が不要になります。 - 正確な並行ベンチマーク:
RunParallel
は、複数のCPUコアを効率的に利用し、並行アクセス下でのコードのパフォーマンスをより正確に測定します。 - データ競合の発見: 並行実行により、シーケンシャルなテストでは見過ごされがちなデータ競合を発見しやすくなります。
- 実世界のシミュレーション: 実際のアプリケーションが並行して動作する状況をより忠実にシミュレートできます。
- 簡潔なコード: 手動でのゴルーチン管理や
runtime.GOMAXPROCS
runtime.GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を設定します。これは、GoのスケジューラがゴルーチンをOSスレッドにマッピングする方法に影響を与えます。RunParallel
はこの値を利用して、最適な並行度を決定します。
ゴルーチンと sync.WaitGroup
- ゴルーチン (Goroutine): Goランタイムによって管理される軽量なスレッドです。数千、数万のゴルーチンを同時に起動してもオーバーヘッドが少ないのが特徴です。
sync.WaitGroup
: 複数のゴルーチンの完了を待機するために使用される同期プリミティブです。Add()
でカウンタを増やし、Done()
で減らし、Wait()
でカウンタがゼロになるまでブロックします。
技術的詳細
このコミットの技術的詳細の中心は、並行ベンチマークのイディオムを、手動でのゴルーチンと sync.WaitGroup
の管理から、testing.B.RunParallel
の利用へと移行した点にあります。
変更前の benchmarkClientServerParallel
関数では、以下の手順で並行ベンチマークを実行していました。
numProcs := runtime.GOMAXPROCS(-1) * conc
で、GOMAXPROCS
の値とconc
(並行度を示す引数) を掛け合わせて、起動するゴルーチンの総数を決定していました。var wg sync.WaitGroup
とwg.Add(numProcs)
でWaitGroup
を初期化し、起動するゴルーチンの数だけカウンタを設定していました。for p := 0; p < numProcs; p++
ループ内で、go func() { ... }()
を使ってnumProcs
個のゴルーチンを起動していました。- 各ゴルーチン内では、
for atomic.AddInt32(&n, -1) >= 0
というループを使って、b.N
回の操作をアトミックカウンタn
を共有しながら実行していました。これにより、すべてのゴルーチンが協力してb.N
回の操作を完了するようにしていました。 - 各ゴルーチンが完了すると
wg.Done()
を呼び出し、最後にwg.Wait()
で全てのゴルーチンの完了を待機していました。
この手動での並行処理の実装は、正しく動作しますが、いくつかの課題があります。
- 複雑性: ゴルーチンの起動、
WaitGroup
による同期、共有カウンタのアトミック操作など、並行処理のロジックがベンチマーク対象のコードとは別に記述され、コードが複雑になります。 - 最適化の機会:
GOMAXPROCS
の値に基づいてゴルーチン数を決定していますが、RunParallel
はGoランタイムのスケジューラとより密接に連携し、より効率的な並行実行を調整できます。 - エラーハンドリング: 各ゴルーチン内でエラーが発生した場合の
b.Logf
の呼び出しやcontinue
の使用は、ベンチマークの全体的な結果に影響を与える可能性があります。
変更後のコードでは、これらの課題が testing.B.RunParallel
を使用することで解決されています。
b.ResetTimer()
: ベンチマーク対象の処理が始まる前にタイマーをリセットします。これはRunParallel
を使う際の一般的なパターンです。b.SetParallelism(parallelism)
:RunParallel
が起動するゴルーチンの数を、引数parallelism
に基づいて設定します。これにより、ベンチマークの並行度を外部から制御できます。b.RunParallel(func(pb *testing.PB) { ... })
: このメソッドが呼び出されると、Goランタイムは指定された並行度(またはデフォルトのGOMAXPROCS
に基づく数)のゴルーチンを起動します。for pb.Next() { ... }
: 各ゴルーチンは、このループ内でベンチマーク対象の操作を実行します。pb.Next()
は、まだ実行すべきイテレーションが残っている場合にtrue
を返します。RunParallel
はb.N
のイテレーションをこれらのゴルーチン間で自動的に分散し、全てのイテレーションが完了するまでループを継続させます。
この変更により、ベンチマークコードはより簡潔になり、Goのテストフレームワークの内部最適化を活用できるようになります。特に、RunParallel
は b.N
のイテレーションを複数のゴルーチンに効率的に割り当てるため、手動でアトミックカウンタを管理する必要がなくなります。
コアとなるコードの変更箇所
src/pkg/net/http/serve_test.go
ファイルの benchmarkClientServerParallel
関数が変更されています。
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -26,7 +26,6 @@ import (
"runtime"
"strconv"
"strings"
- "sync"
"sync/atomic"
"syscall"
"testing"
@@ -2280,42 +2279,33 @@ func BenchmarkClientServerParallel64(b *testing.B) {
benchmarkClientServerParallel(b, 64)
}
-func benchmarkClientServerParallel(b *testing.B, conc int) {
+func benchmarkClientServerParallel(b *testing.B, parallelism int) {
b.ReportAllocs()
- b.StopTimer()
ts := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, r *Request) {
fmt.Fprintf(rw, "Hello world.\n")
}))
defer ts.Close()
- b.StartTimer()
-
- numProcs := runtime.GOMAXPROCS(-1) * conc
- var wg sync.WaitGroup
- wg.Add(numProcs)
- n := int32(b.N)
- for p := 0; p < numProcs; p++ {
- go func() {
- for atomic.AddInt32(&n, -1) >= 0 {
- res, err := Get(ts.URL)
- if err != nil {
- b.Logf("Get: %v", err)
- continue
- }
- all, err := ioutil.ReadAll(res.Body)
- res.Body.Close()
- if err != nil {
- b.Logf("ReadAll: %v", err)
- continue
- }
- body := string(all)
- if body != "Hello world.\n" {
- panic("Got body: " + body)
- }
+ b.ResetTimer()
+ b.SetParallelism(parallelism)
+ b.RunParallel(func(pb *testing.PB) {
+ for pb.Next() {
+ res, err := Get(ts.URL)
+ if err != nil {
+ b.Logf("Get: %v", err)
+ continue
}
- wg.Done()
- }()
- }
- wg.Wait()
+ all, err := ioutil.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ b.Logf("ReadAll: %v", err)
+ continue
+ }
+ body := string(all)
+ if body != "Hello world.\n" {
+ panic("Got body: " + body)
+ }
+ }
+ })
}
// A benchmark for profiling the server without the HTTP client code.
コアとなるコードの解説
変更の核心は、benchmarkClientServerParallel
関数内の並行処理ロジックの書き換えです。
削除されたコード:
import "sync"
:sync.WaitGroup
が不要になったため、sync
パッケージのインポートが削除されました。b.StopTimer()
とb.StartTimer()
:b.ResetTimer()
を使用する新しいパターンでは、これらの明示的なタイマー制御は不要になります。b.ResetTimer()
は、セットアップ処理の後に呼び出され、その時点からベンチマークの計測を開始します。numProcs := runtime.GOMAXPROCS(-1) * conc
: 手動でゴルーチン数を計算するロジックが削除されました。RunParallel
がこの調整を内部的に行います。var wg sync.WaitGroup
とwg.Add(numProcs)
、wg.Done()
、wg.Wait()
:sync.WaitGroup
を使ったゴルーチンの同期メカニズムが完全に削除されました。RunParallel
がゴルーチンのライフサイクルと同期を管理します。n := int32(b.N)
とfor atomic.AddInt32(&n, -1) >= 0
:b.N
のイテレーションを複数のゴルーチンで共有するためのアトミックカウンタとループが削除されました。RunParallel
のpb.Next()
がこの役割を担います。for p := 0; p < numProcs; p++ { go func() { ... }()
: 手動でゴルーチンを起動するループが削除されました。
追加されたコード:
b.ResetTimer()
:httptest.NewServer
のセットアップが完了した後、ベンチマークタイマーをリセットします。これにより、サーバーの起動時間はベンチマークの計測時間に含まれません。b.SetParallelism(parallelism)
:RunParallel
が起動するゴルーチンの数を、関数の引数parallelism
に基づいて設定します。これにより、ベンチマークの並行度を柔軟に制御できます。b.RunParallel(func(pb *testing.PB) { ... })
: この行が、手動の並行処理ロジックの全てを置き換えるものです。無名関数がpb *testing.PB
を引数として受け取り、この関数が複数のゴルーチンで並行して実行されます。for pb.Next() { ... }
:RunParallel
によって起動された各ゴルーチンは、このループ内でベンチマーク対象の操作(この場合はGet(ts.URL)
とレスポンスの読み取り)を実行します。pb.Next()
は、b.N
回の操作が全て完了するまでtrue
を返し続けます。
この変更により、ベンチマークコードは大幅に簡素化され、Goのテストフレームワークの提供する並行ベンチマークの機能が最大限に活用されています。これにより、コードの可読性と保守性が向上し、将来的なGoランタイムの最適化の恩恵も受けやすくなります。
関連リンク
- Go CL: https://golang.org/cl/68070043
参考にした情報源リンク
- konradreiche.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHSgVicdCy4QQs5vSC_J28XCjwETLBvHNl2Wtkpw9ubEx6nYXgGljtmFhWH7k-ELCFrO8ERugld5hVCz7uDt0FXE2aqxNiYhbZ28CGMfd4W4Cr08SyvvKAFe_FZia09d7UjSke87GRM0Z4h7Rt_9pJEOrS=
- jajaldoang.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF2G8sYFgSz1yIfkXMb2Ht9KznYVaW47Rd1IAUw0uvaDx2sEXJDOV8kzeGX17LzVyzwkK7NDuxG0lbYO9uFBn0kRsh3RhKjAiOcr3vFSnBrf1VZ4Ml-ewj_JWYP40YoTfnWpvWqGzDCmjntvJXKryYYAGUOmmrVEwZIc-k=
- stackoverflow.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF5ozN1FMdkabiZDvmTLyLlfQH4EZC1kGuSoG4YSR9aSWsFdVMnSqIFTy8-P1PiLG5VD6NakYHQgGcBUY94RqrITbXT2-cD4Frkz4wjX3BAobSev2yalybvr4K3_a0nmQUp8DPSKJWGL55w3ocncC5DKH7ZHQ39tcg84-EKF9zmGRpB-dGaBZGj-YoOu2tjBPD-nArBqRxK4aYMcB2J6e-bb5khV0Kjz1e2a6w=
- medium.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHK2xG3cSa_8s66UCOah02bMeRZa6OyYaZGoLKmZjJEbH9clMGereXruWF_R1pzY5tVS8RP8itfXp4Ci02cI-xaeqhvDX_frgKGKMdHaM4NFtSg_mX08-Gdw9fTM2Lh479iA9T1hqlAtGqGIuWFS3Jld7Z6TLVx506d3n99-KZrNTFGJ4D4wHt40ZGT57ZZydzgijVBQaL1-6e93JWi5Zvm6G8fdL1tmGmq