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

[インデックス 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.WaitGroupgoroutineとは異なり、ランタイム自身が内部で並列処理を行う際に利用するものです。例えば、ガベージコレクションの並列処理や、特定の最適化されたループ処理などで使用されることがあります。

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を介してアクセスするように変更しました。

  1. グローバル変数 gdata の導入: var gdata []uint64 が追加されました。グローバル変数は、どの関数からも直接アクセスできるため、クロージャの環境としてキャプチャされる必要がありません。
  2. data スライスの gdata への代入: ParForSetupを呼び出す直前に、gdata = dataという行が追加されました。これにより、テスト対象のデータスライスがグローバル変数gdataに設定されます。
  3. 並列ループ本体での 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つのテスト関数、TestParForTestParForParallel における ParForSetup の呼び出し部分です。

  1. var gdata []uint64 の追加:

    • ファイルスコープにgdataというuint64型のスライスを宣言しています。これはグローバル変数として機能します。
    • この変数が導入された目的は、ParForSetupに渡される関数リテラルが、テスト対象のデータスライスdataをクロージャとしてキャプチャするのを避けるためです。
  2. gdata = data の追加:

    • TestParFor および TestParForParallel 関数内で、ParForSetup を呼び出す直前に、ローカル変数であるdataスライスをグローバル変数gdataに代入しています。
    • これにより、dataスライスが指す基底配列への参照がgdataに渡されます。
  3. data := gdata の追加 (関数リテラル内):

    • ParForSetup の最後の引数として渡される関数リテラル(並列ループの本体)の冒頭に、data := gdata という行が追加されました。
    • この行は、グローバル変数gdataが参照しているスライスを、関数リテラル内のローカル変数dataに代入しています。
    • 重要なのは、このdataは関数リテラルのローカル変数であり、外側のスコープのdataとは別物であるという点です。しかし、スライスは参照型であるため、gdataとこのローカルdataは同じ基底配列を指します。
    • これにより、関数リテラルは自身の外側のスコープにあるdataをクロージャとしてキャプチャするのではなく、グローバル変数gdataを介してデータにアクセスする形になります。グローバル変数はレキシカルスコープの外にあるため、この関数リテラルはクロージャとは見なされず、parforが期待する「クロージャではない関数」として振る舞います。

これらの変更により、parfor_test.goは、Goランタイムのparforメカニズムがクロージャを直接扱えないという制約を尊重し、テストがランタイムの実際の挙動と整合するように修正されました。

関連リンク

参考にした情報源リンク

  • Go言語のクロージャに関する公式ドキュメントやチュートリアル
  • Goランタイムの内部構造に関する一般的な知識
  • parforに関するGoのソースコードコメントと関連する議論(もしあれば)