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

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

このコミットは、Go言語のランタイムとリンカにおけるガベージコレクション(GC)のスタックスキャンメカニズムを改善するものです。具体的には、スタックルートのスキャン範囲をローカル変数と引数に限定することで、GCの精度と効率を向上させています。これにより、不要な領域のスキャンを避け、GCのオーバーヘッドを削減することが期待されます。

コミット

commit c676b8b27b128e6369da7c3a3f2bbf52bde945c8
Author: Carl Shapiro <cshapiro@google.com>
Date:   Thu Mar 28 14:36:23 2013 -0700

    cmd/ld, runtime: restrict stack root scan to locals and arguments
    
    Updates #5134
    
    R=golang-dev, bradfitz, cshapiro, daniel.morsing, ality, iant
    CC=golang-dev
    https://golang.org/cl/8022044

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

https://github.com/golang/go/commit/c676b8b27b128e6369da7c3a3f2bbf52bde945c8

元コミット内容

このコミットの元の内容は、Go言語のリンカ (cmd/ld) とランタイム (runtime) において、ガベージコレクタがスタックをスキャンする際に、その対象をローカル変数と関数の引数に限定するというものです。これは、GoのIssue #5134 に対応する変更であり、スタックの正確なスキャンを可能にすることで、ガベージコレクションの効率と正確性を向上させることを目的としています。

変更の背景

Go言語のガベージコレクタは、プログラムが使用しているメモリを自動的に管理し、不要になったメモリを解放する役割を担っています。このプロセスにおいて、GCは「ルート」と呼ばれる、プログラムから直接到達可能なメモリ領域から参照をたどり、到達可能なオブジェクトを「生きている」ものとしてマークします。スタックは、ローカル変数や関数の引数、リターンアドレスなどが格納される重要なルートの一つです。

従来のGoのGCスタックスキャンは、スタックフレーム全体をスキャンする、比較的粗い粒度で行われていました。しかし、スタックフレームにはGCの対象とならないデータ(例えば、レジスタに保存された値のシャドウコピーや、最適化によって不要になった領域など)も含まれる可能性があり、これらをスキャンすることは無駄な処理であり、GCのパフォーマンスに悪影響を与える可能性がありました。

このコミットの背景には、GoのGCの精度と効率をさらに向上させたいという意図があります。特に、スタック上のポインタを正確に識別し、不要な領域をスキャンしないようにすることで、GCの停止時間(STW: Stop-The-World)を短縮し、全体的なアプリケーションの応答性を改善することが求められていました。Issue #5134は、このスタックスキャンの精度に関する課題を提起しており、本コミットはその解決策として提案されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の内部動作に関する前提知識が必要です。

1. Goのガベージコレクション (GC)

GoのGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してマークフェーズとスイープフェーズを実行することで、STW時間を最小限に抑えることを目指しています。

  • マークフェーズ: GCルート(グローバル変数、スタック、レジスタなど)から到達可能なオブジェクトをマークします。
  • スイープフェーズ: マークされなかったオブジェクト(到達不可能なオブジェクト)を「ゴミ」として認識し、メモリを解放します。

2. GCルートとスタックスキャン

GCは、プログラムが現在使用しているすべてのオブジェクトを特定するために、GCルートから参照をたどります。スタックは、現在実行中の関数のローカル変数や引数、リターンアドレスなどが格納されているため、重要なGCルートの一つです。GCは、各ゴルーチンのスタックをスキャンし、スタック上に存在するポインタを識別して、それらが指すオブジェクトをマークする必要があります。

3. スタックフレームと関数呼び出し規約

関数が呼び出されると、その関数に必要な情報(引数、ローカル変数、リターンアドレスなど)を格納するための「スタックフレーム」がスタック上に作成されます。Goのコンパイラとリンカは、このスタックフレームのレイアウトを決定します。

  • 引数 (Arguments): 呼び出し元から渡される値。
  • ローカル変数 (Locals): 関数内で宣言される変数。
  • フレームポインタ (Frame Pointer): 現在のスタックフレームの基点を指すポインタ(アーキテクチャや最適化によっては省略されることもあります)。
  • スタックポインタ (Stack Pointer): スタックの現在のトップを指すポインタ。

4. リンカ (cmd/ld) の役割

Goのリンカは、コンパイルされたオブジェクトファイルを結合し、実行可能ファイルを生成します。この過程で、リンカは関数のスタックフレームに関するメタデータ(例えば、引数のサイズやローカル変数のサイズなど)を生成し、ランタイムがGCのために利用できるようにします。

5. NOSPLIT 関数

Goには、スタックの拡張を伴わない「NOSPLIT」関数という概念があります。これは、非常に短い関数や、スタックの拡張チェックが不要な関数に対して使用されます。NOSPLIT関数は、通常の関数とは異なるスタックフレームの特性を持つ場合があります。

6. runtime.gentraceback

runtime.gentracebackは、Goのランタイムがスタックトレースを生成するために使用する内部関数です。これは、デバッグ情報、プロファイリング、そしてGCのスタックスキャンなど、様々な目的で利用されます。この関数は、スタックフレームを一つずつ遡り、各フレームのPC (Program Counter) やSP (Stack Pointer) などの情報を取得します。

技術的詳細

このコミットの技術的な核心は、Goのガベージコレクタがスタックをスキャンする際の粒度を、スタックフレーム全体から「ローカル変数」と「引数」に限定することにあります。

ArgsSizeUnknown の導入

  • src/cmd/ld/lib.hsrc/pkg/runtime/runtime.hArgsSizeUnknown = 0x80000000 という定数が導入されました。これは、リンカが特定の関数の引数サイズを決定できない場合に設定される特殊な値です。
  • src/cmd/ld/lib.cgenasmsym 関数では、NOSPLIT フラグが設定された関数に対して .args シンボルに ArgsSizeUnknown を設定するようになりました。これは、NOSPLIT 関数がスタック拡張チェックを行わないため、その引数サイズがランタイムから見て不明確になる可能性があるためです。

addframeroots 関数の導入とスタックスキャンの改善

  • src/pkg/runtime/mgc0.caddframeroots という新しい静的関数が導入されました。この関数は、個々のスタックフレームをスキャンし、そのフレーム内のポインタをGCルートとして追加する役割を担います。
  • addframeroots は、以下のロジックでスタックをスキャンします。
    • doframe パラメータが true の場合、または関数のローカル変数サイズ (f->locals) が0の場合、スタックフレーム全体をスキャンします。これは、以前のフレームの引数サイズが不明な場合(ArgsSizeUnknown)に発生します。
    • それ以外の場合、つまり doframefalsef->locals > 0 の場合、ローカル変数のみをスキャンします。
    • 関数の引数サイズ (f->args) が0より大きい場合、引数領域をスキャンします。
    • doframe パラメータは、現在のフレームの引数サイズが ArgsSizeUnknown であるかどうかに応じて更新され、次のフレームのスキャンに影響を与えます。

addstackroots の変更

  • src/pkg/runtime/mgc0.caddstackroots 関数は、スタック全体をスキャンする主要な関数です。この関数は、ScanStackByFrames という新しいコンパイル時フラグ(デフォルトは0、つまり無効)に基づいて動作を変更します。
  • ScanStackByFrames が有効な場合、addstackrootsruntime.gentraceback を呼び出し、そのコールバック関数として addframeroots を渡すようになりました。これにより、gentraceback がスタックフレームを一つずつ遡る際に、各フレームで addframeroots が呼び出され、より粒度の細かいスタックスキャンが実現されます。
  • ScanStackByFrames が無効な場合(現在のデフォルト)、addstackroots は以前と同様にスタックセグメント全体をスキャンします。これは、この変更が段階的に導入され、新しいフレームごとのスキャンロジックが完全に安定するまで、従来の動作を維持するためのものです。

runtime.gentraceback のシグネチャ変更

  • src/pkg/runtime/runtime.h, src/pkg/runtime/traceback_arm.c, src/pkg/runtime/traceback_x86.c において、runtime.gentraceback 関数のシグネチャが変更されました。
  • 新しいシグネチャには、void (*fn)(Func*, byte*, byte*, void*) 型の関数ポインタ fn と、その関数に渡される void* arg が追加されました。
  • この fn 関数ポインタは、gentraceback がスタックフレームを処理する際に呼び出されるコールバック関数として使用されます。GCのスタックスキャンでは、この fnaddframeroots が渡されます。
  • traceback_arm.ctraceback_x86.cgentraceback 実装では、pcbuf == nil かつ fn != nil の場合に (*fn)(f, (byte*)pc, sp, arg) が呼び出されるロジックが追加されました。これは、GCがスタックトレースを生成するのではなく、スタック上のポインタをスキャンするために gentraceback を利用するケースに対応します。

その他の変更

  • src/pkg/runtime/mprof.gocsrc/pkg/runtime/proc.c では、runtime.gentraceback の呼び出し箇所が新しいシグネチャに合わせて更新されました。これらの箇所では、GCのスタックスキャンとは関係なくスタックトレースを生成するため、fnarg には nil が渡されます。
  • runtime.goexit までスタックを巻き戻さないようにするロジックが gentraceback に追加されました。これは、ゴルーチンの終了点を超えてスキャンしないようにするためです。

これらの変更により、GoのGCはスタック上のポインタをより正確に識別し、不要な領域のスキャンを避けることで、GCの効率とパフォーマンスを向上させることが可能になります。特に、ScanStackByFrames フラグが将来的に有効になった場合、この最適化の恩恵を最大限に受けることができます。

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

このコミットのコアとなる変更は、主に以下のファイルに集中しています。

  1. src/cmd/ld/lib.c および src/cmd/ld/lib.h:

    • リンカが関数の引数サイズに関するメタデータを生成する方法を変更。
    • ArgsSizeUnknown 定数の導入。
    • NOSPLIT 関数に対して ArgsSizeUnknown を設定するロジックの追加。
  2. src/pkg/runtime/mgc0.c:

    • ガベージコレクタのスタックスキャンロジックの核心部分。
    • addframeroots 関数の新規追加。
    • addstackroots 関数が ScanStackByFrames フラグに基づいて gentraceback を利用するように変更。
  3. src/pkg/runtime/runtime.h:

    • ArgsSizeUnknown 定数の定義。
    • runtime.gentraceback 関数のシグネチャ変更(新しいコールバック関数引数の追加)。
  4. src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c:

    • runtime.gentraceback の実装が、新しいコールバック関数 fn を利用するように変更。
    • fn が提供された場合に、スタックフレームごとに fn を呼び出すロジックの追加。

コアとなるコードの解説

src/cmd/ld/lib.c (抜粋)

 // ...
 		put(nil, ".frame", 'm', (uint32)s->text->to.offset+PtrSize, 0, 0, 0);
 		put(nil, ".locals", 'm', s->locals, 0, 0, 0);
-		put(nil, ".args", 'm', s->args, 0, 0, 0);
+		if(s->text->textflag & NOSPLIT)
+			put(nil, ".args", 'm', ArgsSizeUnknown, 0, 0, 0);
+		else
+			put(nil, ".args", 'm', s->args, 0, 0, 0);
 // ...

この変更は、リンカが関数のメタデータとして引数サイズを記録する部分です。NOSPLIT フラグが設定されている関数(スタック拡張チェックを行わない関数)の場合、その引数サイズを ArgsSizeUnknown としてマークします。これは、ランタイムがこれらの関数の引数サイズを正確に把握できない可能性があるためです。

src/pkg/runtime/mgc0.c (抜粋)

// ...
// Scan a stack frame.  The doframe parameter is a signal that the previously
// scanned activation has an unknown argument size.  When *doframe is true the
// current activation must have its entire frame scanned.  Otherwise, only the
// locals need to be scanned.
static void
addframeroots(Func *f, byte*, byte *sp, void *doframe)
{
	uintptr outs;

	if(thechar == '5') // For 32-bit systems (e.g., ARM, x86)
		sp += sizeof(uintptr); // Adjust stack pointer for return address

	if(f->locals == 0 || *(bool*)doframe == true)
		// If no locals or previous frame had unknown args, scan entire frame
		addroot((Obj){sp, f->frame - sizeof(uintptr), 0});
	else if(f->locals > 0) {
		// Otherwise, scan only locals
		outs = f->frame - sizeof(uintptr) - f->locals;
		addroot((Obj){sp + outs, f->locals, 0});
	}
	if(f->args > 0)
		// Scan arguments if present
		addroot((Obj){sp + f->frame, f->args, 0});

	// Update doframe for the next frame
	*(bool*)doframe = (f->args == ArgsSizeUnknown);
}

static void
addstackroots(G *gp)
{
// ... (goroutine stack setup)
	if (ScanStackByFrames) {
		bool doframe = false; // Initial state for doframe
		runtime·gentraceback(pc, sp, nil, gp, 0, nil, 0x7fffffff, addframeroots, &doframe);
	} else {
// ... (old stack scanning logic)
	}
}
// ...

addframeroots は、個々のスタックフレームをGCのためにスキャンする新しい関数です。doframe フラグは、前のフレームの引数サイズが不明であった場合に、現在のフレーム全体をスキャンする必要があることを示します。これにより、GCはスタック上のポインタをより正確に識別し、ローカル変数と引数のみに焦点を当ててスキャンできるようになります。

addstackroots は、ScanStackByFrames が有効な場合に runtime.gentraceback を呼び出し、addframeroots をコールバックとして渡すようになりました。これにより、gentraceback がスタックフレームを遡るたびに addframeroots が実行され、フレームごとの精密なスキャンが可能になります。

src/pkg/runtime/runtime.h (抜粋)

// ...
enum
{
	// This value is generated by the linker and should be kept in
	// sync with cmd/ld/lib.h
	ArgsSizeUnknown = 0x80000000,
};
// ...
int32	runtime·gentraceback(byte*, byte*, byte*, G*, int32, uintptr*, int32, void (*)(Func*, byte*, byte*, void*), void*);
// ...

ArgsSizeUnknown は、リンカとランタイム間で共有される定数で、引数サイズが不明であることを示します。runtime.gentraceback のシグネチャ変更は、GCがスタックスキャン時にフレームごとのコールバック関数を登録できるようにするためのものです。

src/pkg/runtime/traceback_x86.c (抜粋)

// ...
// Generic traceback.  Handles runtime stack prints (pcbuf == nil),
// the runtime.Callers function (pcbuf != nil), as well as the garbage
// collector (fn != nil).  A little clunky to merge the two but avoids
// duplicating the code and all its subtlety.
int32
runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr *pcbuf, int32 max, void (*fn)(Func*, byte*, byte*, void*), void *arg)
{
// ...
		else if(fn != nil)
			(*fn)(f, (byte*)pc, sp, arg);
// ...
}

gentraceback の実装では、新しい fn 引数が nil でない場合に、スタックフレームごとにそのコールバック関数を呼び出すロジックが追加されました。これにより、GCは gentraceback を利用してスタックを効率的にトラバースし、各フレームで必要なスキャン処理を実行できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (GoのバージョンによってGCの実装は進化しているため、当時の情報と現在の情報には差異がある可能性があります。)
  • Goのランタイムソースコード (特に src/pkg/runtime ディレクトリ)
  • Goのリンカソースコード (特に src/cmd/ld ディレクトリ)
  • Goのスタック管理に関する技術記事や解説
  • コンパイラとリンカの基本的な概念、特にスタックフレームと関数呼び出し規約について