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

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)のスタック走査メカニズムに関する変更です。具体的には、スタックの走査方法を「フレーム単位」に切り替えるためのフラグを有効にしています。これにより、GCがスタック上のルートオブジェクトを特定する際の精度と効率が向上する可能性があります。

コミット

commit 0e6007e4f91f6c1527c8420ba50056092b58c217
Author: Carl Shapiro <cshapiro@google.com>
Date:   Tue May 14 16:38:12 2013 -0700

    runtime: enable stack scanning by frames
    
    Update #5134
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/9406046

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

https://github.com/golang/go/commit/0e6007e4f91f6c1527c8420ba50056092b58c217

元コミット内容

Goランタイムのガベージコレクタにおいて、スタック走査の挙動を制御するScanStackByFramesという内部フラグの値を0(無効)から1(有効)に変更しています。これにより、スタックの走査がフレーム単位で行われるようになります。また、関連するコードパスでUSED(stk)USED(guard)というマクロが追加されています。

変更の背景

この変更は、Go言語のIssue #5134に関連しています。Issue #5134は、Goのガベージコレクタがスタックを走査する際に、より正確で効率的な方法を導入することを目的としていました。従来のスタック走査は、スタック全体をスキャンするアプローチでしたが、これは不要な領域まで走査したり、ポインタではない値を誤ってポインタと認識するリスクがありました。

「フレーム単位でのスタック走査」を有効にすることで、GCは各関数のスタックフレームの構造をより正確に理解し、そのフレーム内でどこにポインタが存在するかを特定できるようになります。これにより、GCの精度が向上し、誤ったポインタの認識によるバグ(例えば、非ポインタ値をポインタとしてマークしてしまい、本来解放されるべきメモリが解放されないなど)を防ぎ、GCのパフォーマンスも改善されることが期待されます。

前提知識の解説

ガベージコレクション (GC)

ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不可能になった)領域を自動的に解放する仕組みです。これにより、プログラマは手動でのメモリ管理から解放され、メモリリークなどのバグを減らすことができます。Go言語のGCは、並行マーク&スイープ方式をベースとしています。

スタック (Stack)

スタックは、プログラムの実行中にローカル変数、関数呼び出しの引数、戻りアドレスなどを一時的に格納するために使用されるメモリ領域です。関数が呼び出されるたびに新しいスタックフレームが積まれ、関数から戻る際にそのフレームが解放されます。

スタック走査 (Stack Scanning)

ガベージコレクタは、到達可能なオブジェクトを特定するために、プログラムのルートセット(グローバル変数、レジスタ、そしてスタック上の変数)から参照を辿ります。スタック走査は、このルートセットの一部として、現在実行中の関数のスタックフレーム内にあるポインタ(ヒープ上のオブジェクトへの参照)を特定するプロセスです。

ポインタ (Pointer)

ポインタは、メモリ上の特定のアドレスを指し示す変数です。GoのGCは、ポインタを辿って到達可能なオブジェクトをマークします。スタック走査の課題の一つは、スタック上に存在する整数値などが偶然ヒープ上のアドレスと一致した場合に、それを誤ってポインタと認識してしまう「偽陽性(false positive)」の発生です。

スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数の実行に必要な情報(ローカル変数、引数、戻りアドレスなど)がスタック上に確保されます。この領域をスタックフレームと呼びます。各スタックフレームは、特定の関数の呼び出しに対応しています。

runtime·gentraceback

Goランタイム内部の関数で、スタックトレースを生成するために使用されます。この関数は、スタックフレームを辿り、各フレームの情報を取得することができます。スタック走査をフレーム単位で行う場合、この種のトレースバック機能が活用されることがあります。

技術的詳細

このコミットの核心は、src/pkg/runtime/mgc0.cファイル内のScanStackByFramesという定数の値を0から1に変更することです。

// src/pkg/runtime/mgc0.c
enum {
 	Debug = 0,
 	DebugMark = 0,  // run second pass to check mark
 	CollectStats = 0,
-	ScanStackByFrames = 0, // 変更前: 無効
+	ScanStackByFrames = 1, // 変更後: 有効
 	IgnorePreciseGC = 0,

 	// Four bits per word (see #defines below).

この変更により、addstackroots関数内でScanStackByFramestrueと評価されるようになり、runtime·gentraceback関数を用いたスタック走査のパスが有効になります。

// src/pkg/runtime/mgc0.c (抜粋)
addstackroots(G *gp)
{
	// ... (既存のコード) ...

	if (ScanStackByFrames) {
		USED(stk);   // 追加された行
		USED(guard); // 追加された行
		doframe = false;
		runtime·gentraceback(pc, sp, nil, gp, 0, nil, 0x7fffffff, addframeroots, &doframe);
	} else {
		// ... (従来のスタック走査ロジック) ...
	}
}

ScanStackByFramesが有効になると、runtime·gentracebackaddframerootsコールバック関数と共に呼び出されます。これは、スタックを単なるメモリの連続領域としてではなく、個々の関数呼び出しのスタックフレームとして構造的に走査することを意味します。

フレーム単位走査の利点:

  1. 精度向上: 各スタックフレームのレイアウト情報(例えば、コンパイラが生成するスタックマップ)を利用することで、どのメモリ位置が実際にポインタであるかを正確に識別できます。これにより、偽陽性(非ポインタ値を誤ってポインタと認識すること)が大幅に減少します。
  2. 効率性向上: 不要なメモリ領域の走査を避け、ポインタが存在する可能性のある場所のみに焦点を当てることで、GCのスタック走査フェーズのパフォーマンスが向上します。
  3. 安全性: 偽陽性が減少することで、GCが誤って参照を保持していると判断し、本来解放されるべきメモリを解放しないという問題を防ぎます。

USED(stk)USED(guard)の追加は、これらの変数がScanStackByFramesが有効なコードパスで使用されることをコンパイラに通知するためのものです。Goのコンパイラやリンカは、未使用の変数を最適化で削除することがありますが、USEDマクロは、変数が実際には使用されている(ただし、直接的なコード内での参照がない場合がある)ことを示し、最適化による削除を防ぎます。これは、デバッグや特定のランタイムの挙動を制御する際に重要となる場合があります。

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

src/pkg/runtime/mgc0.cファイルの以下の部分が変更されています。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -18,7 +18,7 @@ enum {
  	Debug = 0,
  	DebugMark = 0,  // run second pass to check mark
  	CollectStats = 0,
-	ScanStackByFrames = 0,
+	ScanStackByFrames = 1,
  	IgnorePreciseGC = 0,

  	// Four bits per word (see #defines below).
@@ -1459,6 +1459,8 @@ addstackroots(G *gp)
  		}
  	}
  	if (ScanStackByFrames) {
+\t\tUSED(stk);\n+\t\tUSED(guard);\n \t\tdoframe = false;\n \t\truntime·gentraceback(pc, sp, nil, gp, 0, nil, 0x7fffffff, addframeroots, &doframe);\n \t} else {

コアとなるコードの解説

  1. ScanStackByFrames定数の変更: ScanStackByFramesは、Goランタイムのガベージコレクタがスタックを走査する際に、スタックフレームのメタデータを利用するかどうかを決定する内部フラグです。この値を0から1に変更することで、この機能が有効になります。これは、コンパイル時に決定される挙動であり、ランタイム中に動的に切り替わるものではありません。

  2. addstackroots関数内の条件分岐: addstackroots関数は、GCのマークフェーズ中に、スタック上のルートポインタを特定し、それらが参照するオブジェクトをマークするために呼び出されます。 変更後、if (ScanStackByFrames)の条件が真となるため、runtime·gentraceback関数が呼び出されるパスが実行されます。

  3. USED(stk);USED(guard);の追加: これらはGoランタイムの内部マクロであり、変数stkguardがこのコードパスで「使用されている」ことをコンパイラに明示的に伝えます。これにより、コンパイラがこれらの変数を未使用と判断して最適化によって削除してしまうことを防ぎます。これは、特にデバッグビルドや、特定のランタイムの挙動を維持するために重要です。

  4. runtime·gentracebackの呼び出し: この関数は、現在のゴルーチンのスタックトレースを生成するために使用されます。ここでは、addframerootsというコールバック関数と共に呼び出されています。addframerootsは、runtime·gentracebackがスタックフレームを一つずつ辿るたびに呼び出され、そのフレーム内のポインタを特定し、GCのマークセットに追加する役割を担います。このメカニズムにより、スタックがより構造的に、フレーム単位で走査されるようになります。

この変更は、GoのGCがスタック上のポインタをより正確かつ効率的に識別するための重要なステップであり、GCの全体的な堅牢性とパフォーマンスに寄与します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (特にガベージコレクションに関する部分)
  • Go言語のソースコード (runtimeパッケージ)
  • Go言語のIssueトラッカー (Issue #5134)
  • Go言語のコードレビューシステム (CL 9406046)
  • ガベージコレクションに関する一般的な情報源 (書籍、論文、オンライン記事)
  • スタックフレームとスタック走査に関するコンピュータサイエンスの基礎知識