[インデックス 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.PB
はb.RunParallel
に渡される関数内で使用される構造体で、並列ベンチマークの各イテレーションを制御します。
pb.Next()
: 次のイテレーションに進むためのメソッド。for pb.Next() { ... }
というループで使用され、ベンチマークの各ゴルーチンがb.N
回分の作業を分担して実行できるようにします。
runtime.GOMAXPROCS
runtime.GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を設定または取得する関数です。Go 1.5以降ではデフォルトで利用可能なCPUコア数に設定されますが、それ以前のバージョンではデフォルト値が1でした。並列処理のパフォーマンスを測定する際には、この値を適切に設定することが重要でした。
技術的詳細
このコミットの技術的な核心は、並列ベンチマークの実行ロジックを、手動でのゴルーチン管理からtesting.B.RunParallel
への移行です。
変更前(手動ゴルーチン管理):
変更前のコードでは、以下の手順で並列ベンチマークを実行していました。
runtime.GOMAXPROCS(0)
を呼び出して、現在のGOMAXPROCS
の値(利用可能なCPUコア数)を取得します。b.N
(ベンチマークの総イテレーション数)をGOMAXPROCS
の値で割り、各ゴルーチンが担当するイテレーション数m
を計算します。n
個のゴルーチンを起動し、それぞれがm
回ずつベンチマーク対象の処理(x.decimalString()
)を実行します。- 各ゴルーチンが完了したことをチャネルを通じて通知し、メインのベンチマーク関数がすべてのゴルーチンの完了を待ちます。
この方法は、並列処理の制御を開発者自身が行うため、コードが冗長になり、デッドロックやリソースリークなどのバグを導入するリスクがありました。また、GOMAXPROCS
の値に依存するため、環境によってベンチマークの挙動が変わる可能性もありました。
変更後(testing.B.RunParallel
の使用):
変更後のコードでは、b.RunParallel
関数を使用しています。
b.RunParallel(func(pb *testing.PB) { ... })
を呼び出します。RunParallel
は、GOMAXPROCS
の値に基づいて適切な数のゴルーチンを自動的に起動します。- 各ゴルーチンは、
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
個のゴルーチンを起動しています。各ゴルーチンはm
回x.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言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing testing.B.RunParallel
のドキュメント: https://pkg.go.dev/testing#B.RunParallel- Go 1.2 Release Notes (RunParallelの導入について): https://go.dev/doc/go1.2#testing
参考にした情報源リンク
- 上記の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に移行しています。しかし、コミットメッセージに記載されているため、参考情報として含めます。)