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

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

このコミットは、Goランタイムのスタック成長メカニズムをテストする src/pkg/runtime/stack_test.go ファイルに対する変更です。具体的には、テストの実行時間を短縮し、ファイナライザ内でのスタック成長テストが意図通りに動作するように修正が加えられています。

コミット

runtime: make stack growth test shorter

It runs too long in -short mode.

Disable the one in init, because it doesn't respect -short.

Make the part that claims to test execution in a finalizer actually execute the test in the finalizer.

LGTM=bradfitz R=golang-codereviews, bradfitz CC=aram.h, golang-codereviews, iant, khr https://golang.org/cl/86550045

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

https://github.com/golang/go/commit/9d81ade223e105880853ff31d0e04affd2fec488

元コミット内容

runtime: make stack growth test shorter

It runs too long in -short mode.

Disable the one in init, because it doesn't respect -short.

Make the part that claims to test execution in a finalizer
actually execute the test in the finalizer.

変更の背景

このコミットは、Goランタイムのスタック成長テストである TestStackGrowth が抱えていた2つの主要な問題に対処するために行われました。

  1. -short モードでの実行時間の長さ: Goのテストフレームワークには、go test -short コマンドで実行時間を短縮する「ショートモード」があります。しかし、TestStackGrowth はこのモードを尊重せず、テストが完了するまでに長い時間を要していました。これは、CI/CDパイプラインや開発者のローカル環境での迅速なテスト実行を妨げる要因となっていました。
  2. ファイナライザ内でのテスト実行の不正確さ: TestStackGrowth には、ファイナライザ内でのスタック成長をテストするセクションがありました。しかし、実際のコードでは、スタック成長をトリガーする growStack() 関数がファイナライザのコールバック関数の外で呼び出されており、意図したテストが正確に行われていませんでした。ファイナライザはガベージコレクションによって非同期に実行されるため、その内部でスタック成長が適切に処理されることを確認することは重要です。

これらの問題を解決し、テストの効率性と正確性を向上させることが、このコミットの目的です。

前提知識の解説

Goのスタック成長 (Stack Growth in Go)

Goのゴルーチンは、非常に軽量な実行単位であり、そのスタックは動的に成長・縮小します。これは、従来のOSスレッドが持つ固定サイズの大きなスタックとは対照的です。

  • 初期スタックサイズ: 各ゴルーチンは、通常2KB(Goのバージョンによって異なる場合があります)という小さな初期スタックサイズで開始します。これにより、大量のゴルーチンを生成してもメモリを過度に消費することなく、高い並行性を実現できます。
  • 動的な成長 (Contiguous Stacks): ゴルーチンのスタックが現在の容量に近づくと、Goランタイムは自動的にスタックを拡張します。Go 1.4以降、ランタイムは「連続スタック」戦略を採用しています。
    • 検出: Goコンパイラは、関数の開始時(関数プロローグ)に、後続の関数呼び出しに十分なスタック空間があるかをチェックするコードを挿入します。
    • 割り当てとコピー: より多くのスタック空間が必要な場合、ランタイムは新しい、より大きな連続したメモリブロックを割り当てます(通常、現在のスタックサイズを倍増させます。例:2KBから4KB、次に8KBなど)。古いスタックの内容は、この新しい大きな場所にコピーされ、関連するすべてのポインタが更新されます。
    • 最大サイズ: ゴルーチンのスタックは、64ビットシステムでは最大1GB、32ビットシステムでは250MBまで成長できます。
  • 動的な縮小: Goランタイムはスタックの縮小も管理します。連続スタックモデルでは、スタックの使用量が減少しても明示的なアクションは必要ありません。割り当てられた大きなブロックは、スタックが再び成長した場合に再利用されます。
  • 利点: この動的で効率的なスタック管理は、Goが多数の軽量ゴルーチンを並行してサポートできる主要な要因です。これにより、開発者はスタックサイズの手動管理から解放され、メモリ消費が最適化されます。
  • 適応型スタックサイジング (Go 1.19+): Go 1.19では、新しいゴルーチンの初期スタックサイズを適応的に決定する最適化が導入されました。各ガベージコレクションサイクル後、ランタイムは平均スタック使用量を計算し、その後の新しいゴルーチンは、スタック成長操作(morestack呼び出し)の頻度を減らすために、この平均に近い初期スタックサイズで生成される場合があります。

testing.Short()

testing パッケージはGoの標準テストフレームワークです。testing.Short() 関数は、テストが「ショートモード」で実行されているかどうかを示すブール値を返します。go test -short コマンドでテストを実行すると、この関数は true を返します。開発者はこのフラグを使用して、時間のかかるテストやリソースを大量に消費するテストをショートモードではスキップしたり、テストのイテレーション回数を減らしたりすることで、テストスイート全体の実行時間を短縮できます。

runtime.SetFinalizer

runtime.SetFinalizer は、Goのガベージコレクタ(GC)がオブジェクトが到達不能になったと判断したときに実行される関数をオブジェクトに関連付けるための機能です。その主な目的は、ファイルディスクリプタやcgoを介してCから割り当てられたメモリなど、オブジェクトに関連付けられた非メモリリソースを解放することです。

  • 実行タイミングの予測不能性: ファイナライザがいつ実行されるか、あるいはプログラム終了前に実行されるかどうかの保証はありません。その実行は完全にガベージコレクタのサイクルに依存します。
  • 単一のファイナライザ: 1つのオブジェクトには1つのファイナライザしか関連付けられません。別のファイナライザを設定すると、以前のものが置き換えられます。
  • オブジェクトの復活: ファイナライザが実行されると、それが関連付けられているオブジェクトは再び到達可能になります(ただし、ファイナライザの関連付けはなくなります)。そのオブジェクトは、その後のGCサイクルで到達不能なままであれば、真に回収されます。これは意図せずオブジェクトのライフサイクルを延長する可能性があります。
  • メモリリーク: runtime.SetFinalizer は、特に循環参照がある場合にメモリリークを引き起こす可能性があり、ガベージコレクションを遅らせることがあります。
  • パフォーマンスオーバーヘッド: ファイナライザの使用は、かなりのパフォーマンスコストを招く可能性があります。

Go 1.24で導入された runtime.AddCleanup は、runtime.SetFinalizer の多くの欠点を解決する、より堅牢で柔軟な代替手段です。

sync.WaitGroup

sync.WaitGroup は、Goの並行処理において、複数のゴルーチンの完了を待機するための同期プリミティブです。

  • Add(delta int): 待機するゴルーチンの数を delta だけ増やします。
  • Done(): 完了したゴルーチンの数を1つ減らします。
  • Wait(): WaitGroup のカウンタがゼロになるまでブロックします。

これにより、メインのゴルーチンが他のゴルーチンの処理が完了するのを待つことができます。

技術的詳細

このコミットでは、src/pkg/runtime/stack_test.go ファイルに対して以下の技術的な変更が加えられました。

  1. growStack 関数のイテレーション回数の調整: growStack 関数は、スタックを成長させるために再帰的に呼び出される growStackIter のイテレーション回数を制御します。変更前は常に 1 << 10 (1024回) のイテレーションを行っていました。変更後、testing.Short()true を返す場合(つまりショートモードの場合)は、イテレーション回数を 1 << 8 (256回) に減らすように修正されました。これにより、ショートモードでのテスト実行時間が大幅に短縮されます。

    func growStack() {
    -	for i := 0; i < 1<<10; i++ {
    +	n := 1 << 10
    +	if testing.Short() {
    +		n = 1 << 8
    +	}
    +	for i := 0; i < n; i++ {
    
  2. init() 関数内の growStack() 呼び出しの無効化: テストファイルには、init() 関数内で growStack() を呼び出すセクションがありました。init() 関数はパッケージの初期化時に自動的に実行され、testing.Short() の影響を受けません。そのため、ショートモードであっても常にスタック成長テストがフルで実行されてしまい、テスト時間の短縮という目的と矛盾していました。このコミットでは、この init() 関数全体をコメントアウトすることで、この問題を解決しました。

    // ... and in init
    //func init() {
    //	growStack()
    //}
    
  3. ファイナライザ内でのスタック成長テストの修正: ファイナライザ内でのスタック成長をテストするセクションでは、SetFinalizer に渡される匿名関数(ファイナライザのコールバック)の外側growStack() が呼び出されていました。これでは、ファイナライザが実行される前に growStack() が実行されてしまい、ファイナライザ内でのスタック成長を正確にテストできていませんでした。このコミットでは、growStack() の呼び出しをファイナライザのコールバック関数に移動することで、このテストが意図通りに動作するように修正しました。

    // in finalizer
    	wg.Add(1)
    	go func() {
    		s := new(string)
    		SetFinalizer(s, func(ss *string) {
    +			growStack() // ここに移動
    			done <- true
    		})
    		s = nil
    		runtime.GC()
    		<-done
    		wg.Done()
    	}()
    
  4. wg.Wait() の追加: TestStackGrowth 関数内で、ゴルーチンを起動した直後に wg.Wait() を追加しました。これにより、各ゴルーチンが growStack() を実行し終えるまでメインのテストゴルーチンが待機するようになり、テストの同期が改善され、より信頼性の高い結果が得られるようになりました。

    	// in new goroutine
    	wg.Add(1)
    	go func() {
    		defer wg.Done()
    		growStack()
    	}()
    +	wg.Wait() // 追加
    
    	// in locked goroutine
    	wg.Add(1)
    	go func() {
    		LockOSThread()
    		defer UnlockOSThread()
    		defer wg.Done()
    		growStack()
    		UnlockOSThread()
    	}()
    +	wg.Wait() // 追加
    

これらの変更により、TestStackGrowth はショートモードでより高速に実行され、ファイナライザ内でのスタック成長テストも正確に行われるようになりました。

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

diff --git a/src/pkg/runtime/stack_test.go b/src/pkg/runtime/stack_test.go
index 8add63ad35..9a69704368 100644
--- a/src/pkg/runtime/stack_test.go
+++ b/src/pkg/runtime/stack_test.go
@@ -132,6 +132,7 @@ func TestStackGrowth(t *testing.T) {
  		defer wg.Done()
  		growStack()
  	}()
++	wg.Wait()
  
  	// in locked goroutine
  	wg.Add(1)
@@ -141,6 +142,7 @@ func TestStackGrowth(t *testing.T) {
  		growStack()
  		UnlockOSThread()
  	}()
++	wg.Wait()
  
  	// in finalizer
  	wg.Add(1)
@@ -150,6 +152,7 @@ func TestStackGrowth(t *testing.T) {
  		go func() {
  			s := new(string)
  			SetFinalizer(s, func(ss *string) {
++				growStack()
  				done <- true
  			})
  			s = nil
@@ -163,17 +166,20 @@ func TestStackGrowth(t *testing.T) {
  		}
  	}()
 -
  	wg.Wait()
  }
  
  // ... and in init
 -func init() {
--	growStack()
--}
-+//func init() {
-+//	growStack()
-//}
++//func init() {
++//	growStack()
++//}
  
  func growStack() {
--	for i := 0; i < 1<<10; i++ {
-+	n := 1 << 10
-+	if testing.Short() {
-+		n = 1 << 8
-+	}
-+	for i := 0; i < n; i++ {
  		x := 0
  		growStackIter(&x, i)
  		if x != i+1 {

コアとなるコードの解説

上記の差分は、src/pkg/runtime/stack_test.go ファイルにおける以下の重要な変更を示しています。

  1. wg.Wait() の追加 (行 134, 143): TestStackGrowth 関数内で、新しいゴルーチンとロックされたゴルーチンを起動した直後に wg.Wait() が追加されました。

    • 変更前: ゴルーチンが起動されると、メインのテストゴルーチンはすぐに次の処理に進んでいました。これにより、growStack() の実行が完了する前にテストが終了してしまう可能性があり、テスト結果の信頼性が低下する恐れがありました。
    • 変更後: wg.Wait() を追加することで、メインのテストゴルーチンは、対応する wg.Done() が呼び出されるまで(つまり、起動されたゴルーチンが growStack() の実行を完了するまで)ブロックされるようになりました。これにより、テストの同期が保証され、スタック成長テストが確実に実行されるようになります。
  2. ファイナライザ内での growStack() 呼び出しの移動 (行 152): ファイナライザのテストセクションにおいて、growStack() の呼び出し位置が変更されました。

    • 変更前: SetFinalizer に渡される匿名関数の外側で growStack() が呼び出されていました。これは、ファイナライザが実際に実行される前にスタック成長が起こることを意味し、ファイナライザ内でのスタック成長の挙動をテストするという意図に反していました。
    • 変更後: growStack() の呼び出しが、SetFinalizer のコールバック関数 func(ss *string)内部に移動されました。これにより、ガベージコレクタがオブジェクトを回収し、ファイナライザが実行されたときに初めて growStack() が呼び出されるようになり、ファイナライザ内でのスタック成長が正確にテストされるようになりました。
  3. init() 関数のコメントアウト (行 168-171): ファイル下部に存在した init() 関数が完全にコメントアウトされました。

    • 変更前: init() 関数内で growStack() が呼び出されていました。init() 関数はパッケージの初期化時に自動的に実行され、testing.Short() フラグの影響を受けません。そのため、go test -short でテストを実行しても、この init() 関数内の growStack() は常にフルで実行され、テスト時間の短縮という目的を阻害していました。
    • 変更後: init() 関数をコメントアウトすることで、この不要なスタック成長テストが実行されなくなり、ショートモードでのテスト実行時間が短縮されました。
  4. growStack() 関数のイテレーション回数調整 (行 175-179): growStack() 関数内のループのイテレーション回数が、testing.Short() の値に基づいて動的に変更されるようになりました。

    • 変更前: for i := 0; i < 1<<10; i++ とあり、常に1024回のイテレーションを行っていました。
    • 変更後: n という変数を導入し、testing.Short()true の場合は n1 << 8 (256) に設定し、それ以外の場合は 1 << 10 (1024) に設定するように変更されました。これにより、ショートモードではスタック成長の深さが減少し、テストの実行時間が短縮されます。

これらの変更は、Goランタイムのテストスイートの効率性と正確性を向上させる上で非常に重要です。特に、テストの実行時間を短縮し、特定のシナリオ(ファイナライザ内でのスタック成長)が正しくテストされるようにすることで、開発プロセスをよりスムーズにし、ランタイムの安定性を確保するのに役立ちます。

関連リンク

参考にした情報源リンク