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

[インデックス 18619] ファイルの概要

このコミットは、Go言語の標準ライブラリtimeパッケージ内のベンチマークテストsleep_test.goにおいて、ベンチマークの実行方法をtesting.B.RunParallel関数を使用するように変更するものです。これにより、並行処理のベンチマークがより効率的かつ正確に行えるようになります。

コミット

time: use RunParallel in benchmarks

LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/68060043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bb9531e11ba964fa5f8df2404fea8606a4a43b1d

元コミット内容

src/pkg/time/sleep_test.goファイルにおいて、benchmark関数内のベンチマーク実行ロジックが変更されました。

変更前は、runtime.GOMAXPROCS(-1)で取得した論理CPUの数に応じてP個のgoroutineを起動し、sync.WaitGroupatomic.AddInt32を用いてb.N回分のベンチマーク処理を並行して実行していました。具体的には、b.Nbatchサイズで分割し、各goroutineがatomic.AddInt32(&N, -1) >= 0の条件が満たされるまでbench(batch)を呼び出す形式でした。

変更の背景

Goのtestingパッケージには、並行ベンチマークをより簡単かつ効率的に記述するためのtesting.B.RunParallel関数が用意されています。このコミットは、既存の手動でgoroutineを管理し、sync.WaitGroupatomic操作を用いて並行処理を行うベンチマークコードを、testing.B.RunParallelを使用する形に置き換えることを目的としています。

RunParallelを使用することで、ベンチマークコードが簡潔になり、Goのテストフレームワークが提供する並行処理の管理機能(goroutineの生成、イテレーションの分散、タイミング計測など)を最大限に活用できるようになります。これにより、ベンチマークの信頼性と保守性が向上します。

前提知識の解説

testing.B.RunParallel

testing.B.RunParallelは、Goのtestingパッケージが提供するベンチマーク関数で、並行処理のベンチマークを行うために使用されます。この関数は、GOMAXPROCS(またはb.SetParallelismで設定された値)に基づいて複数のgoroutineを起動し、b.Nで指定されたイテレーション数をこれらのgoroutineに分散して実行させます。

RunParallelに渡される関数は*testing.PB型の引数を受け取ります。このpbオブジェクトのpb.Next()メソッドは、そのgoroutineに割り当てられたイテレーションが残っている限りtrueを返します。ベンチマークコードはfor pb.Next() { ... }のループ内で記述されます。

runtime.GOMAXPROCS

runtime.GOMAXPROCSは、Goランタイムが同時に実行できるOSスレッドの最大数を設定または取得する関数です。引数に-1を渡すと、現在のGOMAXPROCSの値を変更せずに取得できます。これは、Goプログラムが利用できる論理CPUコアの数に相当し、並行処理のベンチマークにおいて、いくつの並行実行単位をシミュレートするかを決定する際に重要な要素となります。

sync.WaitGroup

sync.WaitGroupは、複数のgoroutineの完了を待つための同期プリミティブです。Addメソッドで待機するgoroutineの数を設定し、各goroutineが完了する際にDoneメソッドを呼び出します。Waitメソッドは、すべてのgoroutineがDoneを呼び出すまでブロックします。変更前のコードでは、手動で起動したgoroutineの完了を待つために使用されていました。

atomic.AddInt32

sync/atomicパッケージは、アトミックな(不可分な)操作を提供します。atomic.AddInt32は、int32型の変数に指定された値をアトミックに加算し、その結果を返します。これは、複数のgoroutineから共有されるカウンタなどを安全に更新するために使用されます。変更前のコードでは、b.Nのイテレーション数を複数のgoroutineで共有し、各goroutineが処理するたびにデクリメントするために使用されていました。

testing.PB

testing.PBtesting.B.RunParallelに渡される関数に提供されるオブジェクトで、並行ベンチマークのイテレーション管理を行います。pb.Next()メソッドを通じて、各並行実行単位が次に実行すべきイテレーションがあるかどうかを判断します。

技術的詳細

このコミットの技術的詳細は、Goのベンチマークにおける並行処理の管理方法の進化を示しています。

変更前のアプローチ: 変更前のコードは、GOMAXPROCSの数だけgoroutineを明示的に起動し、sync.WaitGroupatomic.AddInt32を組み合わせて、b.N回のベンチマークイテレーションをこれらのgoroutineに手動で分散させていました。

  • P := runtime.GOMAXPROCS(-1): 利用可能な論理CPU数を取得。
  • N := int32(b.N / batch): 全イテレーション数をバッチサイズで割った回数を計算。
  • for p := 0; p < P; p++ { go func() { ... } }: P個のgoroutineを起動。
  • for atomic.AddInt32(&N, -1) >= 0 { bench(batch) }: 各goroutineがNが0になるまでバッチ処理を実行。atomic.AddInt32Nを安全にデクリメント。
  • wg.Wait(): 全てのgoroutineの完了を待機。

このアプローチは機能しますが、開発者が手動でgoroutineの生成、イテレーションの割り当て、同期を管理する必要があり、コードが複雑になりがちです。また、batchサイズの設定やb.Nの分割方法によっては、ベンチマークの精度や効率に影響を与える可能性がありました。

変更後のアプローチ (testing.B.RunParallelの使用): testing.B.RunParallelを使用することで、これらの複雑な並行処理の管理がGoのテストフレームワークに委ねられます。

  • b.RunParallel(func(pb *testing.PB) { ... }): RunParallelを呼び出し、並行実行されるベンチマークロジックを匿名関数として渡します。
  • for pb.Next() { bench(1000) }: pb.Next()trueを返す限り、ベンチマーク対象の関数bench(1000)が実行されます。RunParallelは、b.Nで指定された総イテレーション数を、内部で起動した複数のgoroutineに自動的に分散します。各goroutineは、自身の担当するイテレーションが完了するまでpb.Next()を呼び出し続けます。

この変更により、以下の利点が得られます。

  1. コードの簡潔性: 手動でのgoroutine管理、WaitGroupatomic操作が不要になり、ベンチマークコードが大幅に簡素化されます。
  2. 信頼性の向上: Goのテストフレームワークが並行処理の管理を最適化するため、手動実装に比べてエラーの発生リスクが低減し、より信頼性の高いベンチマーク結果が得られます。
  3. 効率性: RunParallelは、GOMAXPROCSb.SetParallelismの設定に基づいて最適な数のgoroutineを起動し、イテレーションを効率的に分散します。これにより、並行処理のオーバーヘッドが最小限に抑えられ、より正確なパフォーマンス測定が可能になります。
  4. 標準化: Goのベンチマークのベストプラクティスに沿った記述となり、他の開発者にとっても理解しやすくなります。

この変更は、Goのベンチマークツールが提供する機能を最大限に活用し、より堅牢で保守性の高いベンチマークコードを実現するための典型的なリファクタリングパターンです。

コアとなるコードの変更箇所

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -74,26 +74,13 @@ func benchmark(b *testing.B, bench func(n int)) {
 	for i := 0; i < len(garbage); i++ {\n \t\tgarbage[i] = AfterFunc(Hour, nil)\n \t}\n-\n-\tconst batch = 1000\n-\tP := runtime.GOMAXPROCS(-1)\n-\tN := int32(b.N / batch)\n-\
 \tb.ResetTimer()\
 \n-\tvar wg sync.WaitGroup\n-\twg.Add(P)\n-\n-\tfor p := 0; p < P; p++ {\n-\t\tgo func() {\n-\t\t\tfor atomic.AddInt32(&N, -1) >= 0 {\n-\t\t\t\tbench(batch)\n-\t\t\t}\n-\t\t\twg.Done()\n-\t\t}()\n-\t}\n-\n-\twg.Wait()\
+\tb.RunParallel(func(pb *testing.PB) {\
+\t\tfor pb.Next() {\
+\t\t\tbench(1000)\
+\t\t}\
+\t})\
 \n \tb.StopTimer()\
 \tfor i := 0; i < len(garbage); i++ {\

コアとなるコードの解説

変更はsrc/pkg/time/sleep_test.goファイルのbenchmark関数内で行われています。

変更前:

	const batch = 1000
	P := runtime.GOMAXPROCS(-1)
	N := int32(b.N / batch)

	var wg sync.WaitGroup
	wg.Add(P)

	for p := 0; p < P; p++ {
		go func() {
			for atomic.AddInt32(&N, -1) >= 0 {
				bench(batch)
			}
			wg.Done()
		}()
	}

	wg.Wait()

この部分では、batchサイズを1000と定義し、GOMAXPROCSの数だけgoroutineを起動しています。b.Nbatchで割った回数Natomic.AddInt32で管理し、各goroutineがbench(batch)を繰り返し実行していました。sync.WaitGroupを使って全てのgoroutineの完了を待機しています。

変更後:

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			bench(1000)
		}
	})

変更後は、上記の複雑な手動管理のコードが、b.RunParallelの呼び出し一つに置き換えられています。b.RunParallelに渡される匿名関数内で、for pb.Next() { ... }ループを使ってbench(1000)を実行しています。pb.Next()は、RunParallelが内部で管理するイテレーションが残っている限りtrueを返します。これにより、RunParallelが自動的にgoroutineの生成、イテレーションの分散、そしてベンチマークのタイミング計測を行います。batchサイズはbench(1000)の引数として直接渡されています。

この変更により、ベンチマークコードが大幅に簡素化され、Goのテストフレームワークが提供する並行ベンチマークの機能が活用されるようになりました。

関連リンク

参考にした情報源リンク