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

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

このコミットは、Go言語の標準ライブラリmath/bigパッケージ内のベンチマークコードに対する変更です。具体的には、並列ベンチマークの実行方法を、手動でゴルーチンを管理する方式から、testingパッケージが提供するb.RunParallel関数を使用する方式へと移行しています。これにより、ベンチマークコードの記述が簡潔になり、Goのテストフレームワークの進化に合わせた改善が図られています。

コミット

commit 51b9879a905f35c4572d7cbaa4179b05970de7f5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 24 20:46:56 2014 +0400

    math/big: use RunParallel in benchmarks
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/67830044

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

https://github.com/golang/go/commit/51b9879a905f35c4572d7cbaa4179b05970de7f5

元コミット内容

元のコミットメッセージは以下の通りです。

math/big: use RunParallel in benchmarks

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

このメッセージは、math/bigパッケージのベンチマークにおいて、RunParallel関数を使用するように変更したことを簡潔に示しています。

変更の背景

Go言語の標準ライブラリには、コードのパフォーマンスを測定するためのベンチマーク機能が組み込まれています。初期のGoのベンチマークでは、並列処理のテストを行う際に、開発者が手動でruntime.GOMAXPROCSを設定し、複数のゴルーチンを起動して処理を分散させる必要がありました。これは、並列処理のオーバーヘッドや競合状態の影響を正確に測定するために重要でしたが、ベンチマークコードが複雑になるという欠点がありました。

Go 1.2(2013年12月リリース)で導入されたtesting.B.RunParallel関数は、この手動での並列ベンチマーク設定を簡素化し、より堅牢にするための機能です。この関数は、GOMAXPROCSの値に基づいて自動的に並列実行されるゴルーチンを管理し、各ゴルーチンがベンチマーク対象のコードを効率的に実行できるようにします。

このコミットは、math/bigパッケージのベンチマークコードを、この新しいb.RunParallelのイディオムに適合させることを目的としています。これにより、ベンチマークの記述がよりクリーンになり、将来的なGoのバージョンアップやテストフレームワークの改善にも対応しやすくなります。

前提知識の解説

Go言語のベンチマーク

Go言語では、testingパッケージを使用してベンチマークを記述します。ベンチマーク関数はBenchmarkXxx(*testing.B)というシグネチャを持ち、b.N回ループして対象の処理を実行します。go test -bench=.コマンドで実行され、処理時間と1操作あたりのメモリ割り当てなどを測定します。

testing.B

testing.Bはベンチマーク関数に渡される構造体で、ベンチマークの実行を制御するためのメソッドを提供します。

  • b.N: ベンチマーク対象の処理を繰り返す回数。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。
  • b.RunParallel(body func(pb *testing.PB)): 並列ベンチマークを実行するためのメソッド。このメソッドに渡される関数bodyは、複数のゴルーチンで並行して実行されます。

testing.PB

testing.PBb.RunParallelに渡される関数内で使用される構造体で、並列ベンチマークの各イテレーションを制御します。

  • pb.Next(): 次のイテレーションに進むためのメソッド。for pb.Next() { ... }というループで使用され、ベンチマークの各ゴルーチンがb.N回分の作業を分担して実行できるようにします。

runtime.GOMAXPROCS

runtime.GOMAXPROCSは、Goランタイムが同時に実行できるOSスレッドの最大数を設定または取得する関数です。Go 1.5以降ではデフォルトで利用可能なCPUコア数に設定されますが、それ以前のバージョンではデフォルト値が1でした。並列処理のパフォーマンスを測定する際には、この値を適切に設定することが重要でした。

技術的詳細

このコミットの技術的な核心は、並列ベンチマークの実行ロジックを、手動でのゴルーチン管理からtesting.B.RunParallelへの移行です。

変更前(手動ゴルーチン管理):

変更前のコードでは、以下の手順で並列ベンチマークを実行していました。

  1. runtime.GOMAXPROCS(0)を呼び出して、現在のGOMAXPROCSの値(利用可能なCPUコア数)を取得します。
  2. b.N(ベンチマークの総イテレーション数)をGOMAXPROCSの値で割り、各ゴルーチンが担当するイテレーション数mを計算します。
  3. n個のゴルーチンを起動し、それぞれがm回ずつベンチマーク対象の処理(x.decimalString())を実行します。
  4. 各ゴルーチンが完了したことをチャネルを通じて通知し、メインのベンチマーク関数がすべてのゴルーチンの完了を待ちます。

この方法は、並列処理の制御を開発者自身が行うため、コードが冗長になり、デッドロックやリソースリークなどのバグを導入するリスクがありました。また、GOMAXPROCSの値に依存するため、環境によってベンチマークの挙動が変わる可能性もありました。

変更後(testing.B.RunParallelの使用):

変更後のコードでは、b.RunParallel関数を使用しています。

  1. b.RunParallel(func(pb *testing.PB) { ... })を呼び出します。
  2. RunParallelは、GOMAXPROCSの値に基づいて適切な数のゴルーチンを自動的に起動します。
  3. 各ゴルーチンは、for pb.Next() { ... }ループ内でベンチマーク対象の処理(x.decimalString())を実行します。pb.Next()は、b.N回分の作業がすべてのゴルーチンに均等に分散されるように調整します。

このアプローチにより、開発者は並列処理の低レベルな詳細(ゴルーチンの起動、同期、作業の分割)を意識する必要がなくなり、ベンチマーク対象のロジックに集中できるようになります。RunParallelは、Goのテストフレームワークが提供する最適化と保証(例えば、すべてのゴルーチンがb.N回分の作業を完了すること、適切なウォームアップ期間の確保など)を享受できます。

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

変更はsrc/pkg/math/big/nat_test.goファイル内のBenchmarkStringPiParallel関数に集中しています。

--- a/src/pkg/math/big/nat_test.go
+++ b/src/pkg/math/big/nat_test.go
@@ -437,20 +437,11 @@ func BenchmarkStringPiParallel(b *testing.B) {
 	if x.decimalString() != pi {
 		panic("benchmark incorrect: conversion failed")
 	}
-	n := runtime.GOMAXPROCS(0)
-	m := b.N / n // n*m <= b.N due to flooring, but the error is neglibible (n is not very large)
-	c := make(chan int, n)
-	for i := 0; i < n; i++ {
-		go func() {
-			for j := 0; j < m; j++ {
-				x.decimalString()
-			}
-			c <- 0
-		}()
-	}
-	for i := 0; i < n; i++ {
-		<-c
-	}
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			x.decimalString()
+		}
+	})
 }
 
 func BenchmarkScan10Base2(b *testing.B)     { ScanHelper(b, 2, 10, 10) }

コアとなるコードの解説

変更前のコード

	n := runtime.GOMAXPROCS(0)
	m := b.N / n // n*m <= b.N due to flooring, but the error is neglibible (n is not very large)
	c := make(chan int, n)
	for i := 0; i < n; i++ {
		go func() {
			for j := 0; j < m; j++ {
				x.decimalString()
			}
			c <- 0
		}()
	}
	for i := 0; i < n; i++ {
		<-c
	}
  • n := runtime.GOMAXPROCS(0): 現在のGOMAXPROCSの値を取得し、並列実行するゴルーチンの数を決定しています。
  • m := b.N / n: 各ゴルーチンが実行するイテレーション数を計算しています。b.Nはベンチマークの総イテレーション数です。
  • c := make(chan int, n): ゴルーチンの完了を待つためのバッファ付きチャネルを作成しています。
  • for i := 0; i < n; i++ { go func() { ... }(): n個のゴルーチンを起動しています。各ゴルーチンはmx.decimalString()を呼び出し、完了後にチャネルに値を送信します。
  • for i := 0; i < n; i++ { <-c }: メインのベンチマーク関数が、すべてのゴルーチンがチャネルに値を送信するのを待っています。これにより、すべての並列処理が完了するまでベンチマークが終了しないようにしています。

このコードは、並列処理のロジックを開発者が手動で実装しており、Goの並行処理のプリミティブ(ゴルーチンとチャネル)を直接使用しています。

変更後のコード

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			x.decimalString()
		}
	})
  • b.RunParallel(func(pb *testing.PB) { ... }): testing.Bが提供するRunParallelメソッドを呼び出しています。このメソッドは、引数としてfunc(pb *testing.PB)型の関数を受け取ります。
  • for pb.Next() { ... }: RunParallelによって起動された各ゴルーチンは、このループ内でベンチマーク対象の処理を実行します。pb.Next()は、次のイテレーションに進むべきかどうかを判断し、すべてのb.N回の操作が並列に実行されるように調整します。このループは、各ゴルーチンが担当する作業がなくなるまで継続します。

この変更により、ベンチマークコードは大幅に簡潔になり、Goのテストフレームワークの意図する並列ベンチマークのイディオムに準拠するようになりました。これにより、コードの可読性と保守性が向上し、将来的なフレームワークの改善にも対応しやすくなります。

関連リンク

参考にした情報源リンク

  • 上記のGo言語公式ドキュメント
  • Go言語のベンチマークに関する一般的な記事やチュートリアル(例: "Go Benchmarking" で検索)
  • Go言語のruntime.GOMAXPROCSに関する情報
  • Go言語の並行処理に関する一般的な知識
  • コミットのGitHub URL: https://github.com/golang/go/commit/51b9879a905f35c4572d7cbaa4179b05970de7f5
  • Go CL (Change List) 67830044: https://golang.org/cl/67830044 (これは古いGoのコードレビューシステムへのリンクであり、現在はGerritに移行しています。しかし、コミットメッセージに記載されているため、参考情報として含めます。)