[インデックス 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.h
とsrc/pkg/runtime/runtime.h
にArgsSizeUnknown = 0x80000000
という定数が導入されました。これは、リンカが特定の関数の引数サイズを決定できない場合に設定される特殊な値です。src/cmd/ld/lib.c
のgenasmsym
関数では、NOSPLIT
フラグが設定された関数に対して.args
シンボルにArgsSizeUnknown
を設定するようになりました。これは、NOSPLIT
関数がスタック拡張チェックを行わないため、その引数サイズがランタイムから見て不明確になる可能性があるためです。
addframeroots
関数の導入とスタックスキャンの改善
src/pkg/runtime/mgc0.c
にaddframeroots
という新しい静的関数が導入されました。この関数は、個々のスタックフレームをスキャンし、そのフレーム内のポインタをGCルートとして追加する役割を担います。addframeroots
は、以下のロジックでスタックをスキャンします。doframe
パラメータがtrue
の場合、または関数のローカル変数サイズ (f->locals
) が0の場合、スタックフレーム全体をスキャンします。これは、以前のフレームの引数サイズが不明な場合(ArgsSizeUnknown
)に発生します。- それ以外の場合、つまり
doframe
がfalse
でf->locals > 0
の場合、ローカル変数のみをスキャンします。 - 関数の引数サイズ (
f->args
) が0より大きい場合、引数領域をスキャンします。 doframe
パラメータは、現在のフレームの引数サイズがArgsSizeUnknown
であるかどうかに応じて更新され、次のフレームのスキャンに影響を与えます。
addstackroots
の変更
src/pkg/runtime/mgc0.c
のaddstackroots
関数は、スタック全体をスキャンする主要な関数です。この関数は、ScanStackByFrames
という新しいコンパイル時フラグ(デフォルトは0、つまり無効)に基づいて動作を変更します。ScanStackByFrames
が有効な場合、addstackroots
はruntime.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のスタックスキャンでは、このfn
にaddframeroots
が渡されます。 traceback_arm.c
とtraceback_x86.c
のgentraceback
実装では、pcbuf == nil
かつfn != nil
の場合に(*fn)(f, (byte*)pc, sp, arg)
が呼び出されるロジックが追加されました。これは、GCがスタックトレースを生成するのではなく、スタック上のポインタをスキャンするためにgentraceback
を利用するケースに対応します。
その他の変更
src/pkg/runtime/mprof.goc
とsrc/pkg/runtime/proc.c
では、runtime.gentraceback
の呼び出し箇所が新しいシグネチャに合わせて更新されました。これらの箇所では、GCのスタックスキャンとは関係なくスタックトレースを生成するため、fn
とarg
にはnil
が渡されます。runtime.goexit
までスタックを巻き戻さないようにするロジックがgentraceback
に追加されました。これは、ゴルーチンの終了点を超えてスキャンしないようにするためです。
これらの変更により、GoのGCはスタック上のポインタをより正確に識別し、不要な領域のスキャンを避けることで、GCの効率とパフォーマンスを向上させることが可能になります。特に、ScanStackByFrames
フラグが将来的に有効になった場合、この最適化の恩恵を最大限に受けることができます。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
-
src/cmd/ld/lib.c
およびsrc/cmd/ld/lib.h
:- リンカが関数の引数サイズに関するメタデータを生成する方法を変更。
ArgsSizeUnknown
定数の導入。NOSPLIT
関数に対してArgsSizeUnknown
を設定するロジックの追加。
-
src/pkg/runtime/mgc0.c
:- ガベージコレクタのスタックスキャンロジックの核心部分。
addframeroots
関数の新規追加。addstackroots
関数がScanStackByFrames
フラグに基づいてgentraceback
を利用するように変更。
-
src/pkg/runtime/runtime.h
:ArgsSizeUnknown
定数の定義。runtime.gentraceback
関数のシグネチャ変更(新しいコールバック関数引数の追加)。
-
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 Issue #5134: https://github.com/golang/go/issues/5134
- Go Change List 8022044: https://golang.org/cl/8022044 (これは古いGoのコードレビューシステムへのリンクであり、現在はGitHubのコミットページにリダイレクトされるか、アクセスできない場合があります。)
参考にした情報源リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (GoのバージョンによってGCの実装は進化しているため、当時の情報と現在の情報には差異がある可能性があります。)
- Goのランタイムソースコード (特に
src/pkg/runtime
ディレクトリ) - Goのリンカソースコード (特に
src/cmd/ld
ディレクトリ) - Goのスタック管理に関する技術記事や解説
- コンパイラとリンカの基本的な概念、特にスタックフレームと関数呼び出し規約について