[インデックス 15374] ファイルの概要
このコミットは、Go言語のランタイムパッケージ内の並列処理テスト(parfor_test.go
)において、クロージャの使用を避けるための変更を導入しています。具体的には、ParForSetup
関数に渡される並列ループの本体がクロージャとして機能しないように修正し、テストの堅牢性とランタイムの挙動との整合性を高めることを目的としています。
コミット
commit 0ba5f755132386604f811aaaf587cfc65a36bc38
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 22 12:11:12 2013 -0500
runtime: avoid closure in parfor test
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/7395051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0ba5f755132386604f811aaaf587cfc65a36bc38
元コミット内容
runtime: avoid closure in parfor test
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/7395051
変更の背景
この変更の背景には、Goランタイムの並列処理メカニズム(parfor
)がクロージャを直接的に、あるいは効率的に扱えないという制約が存在したことが挙げられます。
parfor
は、Goランタイム内部で並列ループを効率的に実行するための低レベルなメカニズムです。このような低レベルなコードは、パフォーマンスが非常に重要であり、また、C言語で書かれた部分(Goランタイムの多くはCで書かれているか、Cとの連携を前提としている)との相互運用性も考慮されることがあります。
元のテストコードでは、ParForSetup
に渡される関数リテラルが、その外側のスコープで定義されたdata
スライス変数を参照していました。これはGo言語における典型的な「クロージャ」の振る舞いです。しかし、コミットメッセージのコメント「// Avoid making func a closure: parfor cannot invoke them.
」が示唆するように、parfor
の内部実装はクロージャの呼び出しをサポートしていないか、あるいはその呼び出しに非効率性があると考えられます。
テストコードは、対象となる機能の正しい動作を検証するだけでなく、その機能が意図された方法で使われているか、あるいは特定の制約下でどのように振る舞うかを反映すべきです。もしparfor
がクロージャを効率的に扱えない、または全く扱えないのであれば、テストコードもその制約を反映し、クロージャではない形式で並列ループの本体を記述することが適切です。これにより、テストがランタイムの実際の挙動と乖離することを防ぎ、将来的な問題の発生を未然に防ぐことができます。
この変更は、テストの正確性を保ちつつ、ランタイムの設計上の制約を明確にするためのものです。
前提知識の解説
Go言語におけるクロージャ (Closure)
Go言語において、クロージャとは、関数が自身の外側のスコープにある変数を参照できる機能を持つ関数リテラルのことです。関数リテラルが定義された時点の環境(レキシカルスコープ)を「閉じ込める(capture)」ため、「クロージャ」と呼ばれます。
例:
func outer() func() int {
x := 0
return func() int { // この関数リテラルがクロージャ
x++
return x
}
}
func main() {
f := outer()
fmt.Println(f()) // 1
fmt.Println(f()) // 2
}
この例では、outer
関数内で定義された匿名関数が、outer
のローカル変数x
を参照し、変更しています。f
が呼び出されるたびにx
の値が保持され、インクリメントされます。
クロージャは非常に強力で柔軟な機能ですが、その実装には、参照される変数をヒープに割り当てたり、関数ポインタだけでなく環境ポインタ(コンテキスト)も渡す必要があるなど、通常の関数呼び出しとは異なるオーバーヘッドや複雑さが伴う場合があります。
Goランタイムの parfor
(Parallel For)
parfor
は、Go言語のランタイム(runtime
パッケージ)内部で使用される、並列処理のための低レベルなプリミティブです。これは、Goのユーザーが直接利用するsync.WaitGroup
やgoroutine
とは異なり、ランタイム自身が内部で並列処理を行う際に利用するものです。例えば、ガベージコレクションの並列処理や、特定の最適化されたループ処理などで使用されることがあります。
parfor
は、与えられたタスクを複数のプロセッサ(またはGoスケジューラが管理するP)に分散して実行するためのフレームワークを提供します。その目的は、可能な限りオーバーヘッドを少なくし、効率的に並列処理を実行することです。
GOMAXPROCS
GOMAXPROCS
は、Goプログラムが同時に実行できるOSスレッドの最大数を制御する環境変数、またはruntime.GOMAXPROCS
関数です。Goスケジューラは、この値に基づいてgoroutineをOSスレッドにマッピングし、並列実行を管理します。GOMAXPROCS
の値を増やすことで、より多くのCPUコアを利用して並列処理を行うことが可能になります。
このコミットのテストコードではP := GOMAXPROCS(-1)
という記述があり、これは現在のGOMAXPROCS
の値を読み取るために使われています。
技術的詳細
このコミットの技術的詳細は、Goランタイムのparfor
メカニズムがクロージャを直接サポートしていない、またはその使用が推奨されないという点に集約されます。
Goの関数リテラルがクロージャとして機能する場合、その関数は、キャプチャした変数を保持するための「環境」または「コンテキスト」へのポインタを内部的に持ちます。関数が呼び出される際には、この環境ポインタも一緒に渡され、関数本体はそのポインタを介してキャプチャされた変数にアクセスします。
parfor
のような低レベルな並列処理フレームワークでは、パフォーマンスを最大化するために、可能な限りシンプルな関数ポインタの呼び出しを期待している可能性があります。クロージャの環境ポインタの管理や、異なるゴルーチン間でのクロージャ環境の共有(特に書き込み可能な変数をキャプチャしている場合)は、複雑性や同期の問題を引き起こす可能性があります。
このコミットでは、この問題を回避するために、ParForSetup
に渡す関数リテラルがクロージャにならないように修正しています。具体的には、関数リテラルが参照していたdata
スライスを、グローバル変数gdata
を介してアクセスするように変更しました。
- グローバル変数
gdata
の導入:var gdata []uint64
が追加されました。グローバル変数は、どの関数からも直接アクセスできるため、クロージャの環境としてキャプチャされる必要がありません。 data
スライスのgdata
への代入:ParForSetup
を呼び出す直前に、gdata = data
という行が追加されました。これにより、テスト対象のデータスライスがグローバル変数gdata
に設定されます。- 並列ループ本体での
gdata
の利用:ParForSetup
に渡される関数リテラル内で、data := gdata
という行が追加されました。これは、グローバル変数gdata
の値をローカル変数data
にコピーしているように見えますが、実際にはgdata
が参照している基底配列への参照をdata
に代入しています。これにより、関数リテラルは自身の外側のスコープにあるdata
スライスを直接キャプチャするのではなく、グローバル変数gdata
を介してデータにアクセスします。グローバル変数はレキシカルスコープの外にあるため、この関数リテラルはクロージャとは見なされません。
この変更により、parfor
はクロージャの複雑な呼び出しメカニズムを考慮することなく、シンプルな関数ポインタとして並列ループの本体を呼び出すことができるようになります。これは、ランタイムのテストが、ランタイムの実際の制約と挙動に合致していることを保証するための重要な修正です。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/parfor_test.go
+++ b/src/pkg/runtime/parfor_test.go
@@ -13,6 +13,8 @@ import (
"unsafe"
)
+var gdata []uint64
+
// Simple serial sanity test for parallelfor.
func TestParFor(t *testing.T) {
const P = 1
@@ -22,7 +24,12 @@ func TestParFor(t *testing.T) {
data[i] = i
}
desc := NewParFor(P)
+ // Avoid making func a closure: parfor cannot invoke them.
+ // Since it doesn't happen in the C code, it's not worth doing
+ // just for the test.
+ gdata = data
ParForSetup(desc, P, N, nil, true, func(desc *ParFor, i uint32) {
+ data := gdata
data[i] = data[i]*data[i] + 1
})
ParForDo(desc)
@@ -111,7 +118,9 @@ func TestParForParallel(t *testing.T) {
P := GOMAXPROCS(-1)
c := make(chan bool, P)
desc := NewParFor(uint32(P))
+ gdata = data
ParForSetup(desc, uint32(P), uint32(N), nil, false, func(desc *ParFor, i uint32) {
+ data := gdata
data[i] = data[i]*data[i] + 1
})
for p := 1; p < P; p++ {
コアとなるコードの解説
このコミットのコアとなる変更は、src/pkg/runtime/parfor_test.go
ファイル内の2つのテスト関数、TestParFor
と TestParForParallel
における ParForSetup
の呼び出し部分です。
-
var gdata []uint64
の追加:- ファイルスコープに
gdata
というuint64
型のスライスを宣言しています。これはグローバル変数として機能します。 - この変数が導入された目的は、
ParForSetup
に渡される関数リテラルが、テスト対象のデータスライスdata
をクロージャとしてキャプチャするのを避けるためです。
- ファイルスコープに
-
gdata = data
の追加:TestParFor
およびTestParForParallel
関数内で、ParForSetup
を呼び出す直前に、ローカル変数であるdata
スライスをグローバル変数gdata
に代入しています。- これにより、
data
スライスが指す基底配列への参照がgdata
に渡されます。
-
data := gdata
の追加 (関数リテラル内):ParForSetup
の最後の引数として渡される関数リテラル(並列ループの本体)の冒頭に、data := gdata
という行が追加されました。- この行は、グローバル変数
gdata
が参照しているスライスを、関数リテラル内のローカル変数data
に代入しています。 - 重要なのは、この
data
は関数リテラルのローカル変数であり、外側のスコープのdata
とは別物であるという点です。しかし、スライスは参照型であるため、gdata
とこのローカルdata
は同じ基底配列を指します。 - これにより、関数リテラルは自身の外側のスコープにある
data
をクロージャとしてキャプチャするのではなく、グローバル変数gdata
を介してデータにアクセスする形になります。グローバル変数はレキシカルスコープの外にあるため、この関数リテラルはクロージャとは見なされず、parfor
が期待する「クロージャではない関数」として振る舞います。
これらの変更により、parfor_test.go
は、Goランタイムのparfor
メカニズムがクロージャを直接扱えないという制約を尊重し、テストがランタイムの実際の挙動と整合するように修正されました。
関連リンク
- Go CL (Code Review) 7395051: https://golang.org/cl/7395051
参考にした情報源リンク
- Go言語のクロージャに関する公式ドキュメントやチュートリアル
- Goランタイムの内部構造に関する一般的な知識
parfor
に関するGoのソースコードコメントと関連する議論(もしあれば)