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

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

このコミットは、Goランタイムのガベージコレクション(GC)におけるスタックルートのスキャン方法を改善するものです。具体的には、スタック上のルートポインタのスキャン範囲を、関数のローカル変数と引数に限定することで、GCの精度と効率を向上させています。これにより、不要な領域のスキャンを避け、GCのパフォーマンスと正確性を高めることを目的としています。

コミット

commit db018bf77cba282220d852f19203570ae5e87c37
Author: Carl Shapiro <cshapiro@google.com>
Date:   Mon Mar 4 19:48:50 2013 -0800

    runtime: restrict stack root scan to locals and arguments
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/7301062
---
 src/pkg/runtime/mgc0.c          | 59 +++++++++++++++++++++--------------------\
 src/pkg/runtime/mprof.goc       |  2 +-\
 src/pkg/runtime/proc.c          |  2 +-\
 src/pkg/runtime/runtime.h       |  2 +-\
 src/pkg/runtime/sigqueue.goc    |  3 ++-\
 src/pkg/runtime/traceback_arm.c | 29 +++++++++++++-------\
 src/pkg/runtime/traceback_x86.c | 37 ++++++++++++++++----------
 7 files changed, 77 insertions(+), 57 deletions(-)

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

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

元コミット内容

runtime: restrict stack root scan to locals and arguments

変更の背景

Goのガベージコレクタは、到達可能なオブジェクトを特定するために、プログラムのルート(グローバル変数、レジスタ、スタック上のポインタなど)からヒープをスキャンします。このコミット以前のGoランタイムでは、スタックのスキャンがより保守的であった可能性があります。つまり、スタック上のすべてのメモリ領域をポインタとして扱うか、あるいはポインタではないデータも誤ってポインタとして解釈するリスクがありました。

このような保守的なスキャンは、以下の問題を引き起こす可能性があります。

  1. 非効率性: スタック上のポインタではないデータや、到達不能なオブジェクトを指す可能性のある古いポインタをスキャンすることで、GCのオーバーヘッドが増加します。
  2. 不正確性: 誤って非ポインタデータをポインタとして解釈し、その結果、到達不能なオブジェクトを誤って到達可能と判断してしまう「ポインタの誤認識」が発生する可能性があります。これは、メモリリークやGCの効率低下につながります。

このコミットは、スタック上のポンスキャンを「ローカル変数と引数」に限定することで、これらの問題を解決しようとしています。これは、GoのGCがより「正確なGC」に近づくための一歩であり、スタックフレーム内のどこにポインタが存在するかをより正確に知ることで、スキャン範囲を最適化し、GCのパフォーマンスと信頼性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステム。ガベージコレクション、スケジューリング、メモリ管理など、Go言語のコア機能を提供します。C言語で書かれた部分が多く、特にGCやスケック管理はC言語で実装されています。
  • ガベージコレクション (Garbage Collection, GC): プログラムが動的に割り当てたメモリのうち、もはや使用されていない(到達不能な)領域を自動的に解放するプロセス。GoのGCは、並行マーク&スイープ方式を採用しています。
  • ルートポインタ (Root Pointers): GCが到達可能性を判断する際の起点となるポインタ。これには、グローバル変数、CPUレジスタ、そして実行中のゴルーチンのスタック上に存在するポインタなどが含まれます。GCはこれらのルートからヒープ上のオブジェクトを辿り、到達可能なオブジェクトをマークします。
  • スタック (Stack): 関数呼び出しの際に、ローカル変数、引数、戻りアドレスなどが一時的に格納されるメモリ領域。Goでは、ゴルーチンごとに独立したスタックを持ちます。
  • スタックフレーム (Stack Frame): 関数が呼び出されるたびにスタック上に作成される、その関数固有のメモリ領域。これには、関数の引数、ローカル変数、呼び出し元の戻りアドレスなどが含まれます。
  • gentraceback 関数: Goランタイム内部で使用される関数で、スタックトレースを生成するために利用されます。デバッグ情報、プロファイリング、そしてガベージコレクションにおけるスタックのスキャンなど、様々な目的で呼び出しスタックを辿る際に使用されます。この関数は、スタック上の各フレームを解析し、そのフレームのPC (Program Counter) やSP (Stack Pointer) などの情報を取得します。
  • 保守的なGC (Conservative GC): メモリ領域がポインタであるかどうかを確実に判断できない場合でも、それがポインタである可能性があると仮定してスキャンするGC方式。実装が容易ですが、誤って非ポインタをポインタと解釈するリスクがあり、メモリリークの原因となることがあります。
  • 正確なGC (Precise GC): メモリ領域がポインタであるかどうかを正確に判断できるGC方式。コンパイラやランタイムがポインタの正確な位置情報をGCに提供することで実現されます。より効率的で正確なメモリ管理が可能ですが、実装は複雑になります。

このコミットは、gentraceback関数を拡張し、スタックフレーム内のポインタをより正確に特定するためのコールバックメカニズムを導入することで、スタックのスキャンをより正確なものにしようとしています。

技術的詳細

このコミットの主要な変更点は、Goランタイムのガベージコレクタがスタックをスキャンする方法を根本的に変更したことです。

以前のaddstackroots関数(src/pkg/runtime/mgc0.c内)は、Stktop構造体とスタックポインタ(sp)とガード(guard)を使って、スタックセグメント全体を線形にスキャンしていました。これは、スタック上のどこにポインタがあるかを正確に知らずに、ある程度の範囲を「ポインタかもしれない」と仮定してスキャンする、より保守的なアプローチでした。

このコミットでは、addstackroots関数が大幅に簡素化され、代わりにruntime·gentraceback関数を呼び出すようになりました。runtime·gentraceback関数は、スタックトレースを生成するための汎用的な関数ですが、この変更により、スタック上のルートポインタを特定するための新しい役割が与えられました。

新しいruntime·gentraceback関数は、void (*fn)(Func*, byte*, byte*, void*)という新しい引数を受け取るようになりました。これは、スタックフレームが見つかるたびに呼び出されるコールバック関数です。このコールバック関数fnは、見つかった関数の情報(Func*)、その関数のPC(byte*)、SP(byte*)、そして任意のユーザーデータ(void*)を受け取ります。

addstackroots関数では、このコールバックとしてaddframerootsという新しい静的関数を渡しています。addframeroots関数は、Func構造体から取得できるf->frame(フレームサイズ)とf->args(引数のサイズ)の情報を使って、スタックフレーム内のローカル変数と引数の領域を正確に特定し、その領域をaddroot関数に渡してルートポインタとして登録します。

これにより、GCはスタック全体を盲目的にスキャンするのではなく、コンパイラが生成した関数メタデータ(Func構造体)に基づいて、ポインタが存在する可能性のある正確な領域(ローカル変数と引数)のみをスキャンするようになりました。これは、GoのGCがより正確なスタックスキャンを実行するための重要なステップです。

また、runtime·gentraceback関数自体も、pcbuf == nil(スタックトレースの表示目的)とfn == nil(GC目的)の条件分岐が追加され、GC目的の呼び出しでは、スタックセグメントの境界やnewstack/lessstackのようなランタイム内部のスタック操作に関するデバッグ出力を行わないようになりました。

さらに、runtime·goexit(ゴルーチンの終了点)に到達した場合は、それ以上スタックをアンワインドしないようにするブレーク条件が追加され、スタックトレースの終端がより明確になりました。

mprof.gocproc.cruntime.hsigqueue.goctraceback_arm.ctraceback_x86.cの変更は、主にruntime·gentraceback関数のシグネチャ変更(新しいコールバック引数の追加)と、その呼び出し箇所の更新です。特に、mprof.gocproc.cでは、プロファイリング目的でgentracebackを呼び出す際に、新しいコールバック引数にnilを渡すように変更されています。

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

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1299,52 +1299,53 @@ addroot(Obj obj)
 	work.nroot++;
 }
 
+static void
+addframeroots(Func *f, byte*, byte *sp, void*)
+{
+	if (f->frame > sizeof(uintptr))
+		addroot((Obj){sp, f->frame - sizeof(uintptr), 0});
+	if (f->args > 0)
+		addroot((Obj){sp + f->frame, f->args, 0});
+}
+
 static void
 addstackroots(G *gp)
 {
 	M *mp;
--	int32 n;
--	Stktop *stk;
--	byte *sp, *guard;
--
--	stk = (Stktop*)gp->stackbase;
--	guard = (byte*)gp->stackguard;
++	Func *f;
++	byte *sp, *pc;
 
 	if(gp == g) {
 		// Scanning our own stack: start at &gp.
 		sp = (byte*)&gp;
++		pc = runtime·getcallerpc(&gp);
 	} else if((mp = gp->m) != nil && mp->helpgc) {
 		// gchelper's stack is in active use and has no interesting pointers.
 		return;
++	} else if(gp->gcstack != (uintptr)nil) {
++		// Scanning another goroutine that is about to enter or might
++		// have just exited a system call. It may be executing code such
++		// as schedlock and may have needed to start a new stack segment.
++		// Use the stack segment and stack pointer at the time of
++		// the system call instead, since that won't change underfoot.
++		sp = (byte*)gp->gcsp;
++		pc = gp->gcpc;
 	} else {
 		// Scanning another goroutine's stack.
 		// The goroutine is usually asleep (the world is stopped).
 		sp = (byte*)gp->sched.sp;
--
--		// The exception is that if the goroutine is about to enter or might
--		// have just exited a system call, it may be executing code such
--		// as schedlock and may have needed to start a new stack segment.
--		// Use the stack segment and stack pointer at the time of
--		// the system call instead, since that won't change underfoot.
--		if(gp->gcstack != (uintptr)nil) {
--			stk = (Stktop*)gp->gcstack;
--			sp = (byte*)gp->gcsp;
--			guard = (byte*)gp->gcguard;
--		}
--	}
--
--	n = 0;
--	while(stk) {
--		if(sp < guard-StackGuard || (byte*)stk < sp) {
--			runtime·printf("scanstack inconsistent: g%D#%d sp=%p not in [%p,%p]\n", gp->goid, n, sp, guard-StackGuard, stk);
--			runtime·throw("scanstack");
++		pc = gp->sched.pc;
++		if (pc == (byte*)runtime·goexit && gp->fnstart != nil) {
++			// The goroutine has not started.  Its incoming
++			// arguments are at the top of the stack and must
++			// be scanned.  No other data on the stack.
++			f = runtime·findfunc((uintptr)gp->fnstart->fn);
++			if (f->args > 0)
++				addroot((Obj){sp, f->args, 0});
++			return;
 		}
--		addroot((Obj){sp, (byte*)stk - sp, 0});
--		sp = (byte*)stk->gobuf.sp;
--		guard = stk->stackguard;
--		stk = (Stktop*)stk->stackbase;
--		n++;
 	}
++	runtime·gentraceback(pc, sp, nil, gp, 0, nil, 0x7fffffff, addframeroots, nil);
 }

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -748,7 +748,7 @@ void	runtime·exitsyscall(void);
 G*	runtime·newproc1(FuncVal*, byte*, int32, int32, void*);
 bool	runtime·sigsend(int32 sig);
 int32	runtime·callers(int32, uintptr*, int32);
-int32	runtime·gentraceback(byte*, byte*, byte*, G*, int32, uintptr*, int32);
+int32	runtime·gentraceback(byte*, byte*, byte*, G*, int32, uintptr*, int32,  void (*fn)(Func*, byte*, byte*, void*), void *arg);
 int64	runtime·nanotime(void);
 void	runtime·dopanic(int32);
 void	runtime·startpanic(int32);

src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c

これらのファイルでは、runtime·gentraceback関数のシグネチャが変更され、新しいコールバック関数ポインタfnと引数argが追加されています。また、スタックトレースのループ条件がforからwhileに変更され、iterによる無限ループ防止のカウンタが削除されています。これは、コールバックベースの処理により、スタックセグメントの境界を越えても安全に処理できるようになったためと考えられます。

さらに、pcbuf == nil && fn == nilという条件が追加され、GC目的の呼び出し(fn != nilの場合)では、スタックセグメント境界の表示やnewstack/lessstackに関するデバッグ出力を行わないように変更されています。

重要な変更として、if(fn != nil)のブロックが追加され、コールバック関数が指定されている場合は、pcbufにPCを格納する代わりに、コールバック関数(*fn)(f, (byte*)pc, sp, arg)を呼び出すようになりました。

また、スタックの最下部(runtime·goexit)に到達した場合に、それ以上アンワインドしないようにするブレーク条件が追加されています。

コアとなるコードの解説

addframeroots 関数 (新設)

static void
addframeroots(Func *f, byte*, byte *sp, void*)
{
	if (f->frame > sizeof(uintptr))
		addroot((Obj){sp, f->frame - sizeof(uintptr), 0});
	if (f->args > 0)
		addroot((Obj){sp + f->frame, f->args, 0});
}

この関数は、runtime·gentracebackからコールバックとして呼び出されます。

  • f->frame: 現在のスタックフレームの合計サイズ(引数、ローカル変数、戻りアドレスなどを含む)。
  • f->args: 関数の引数の合計サイズ。

addframerootsは、以下の2つの領域をルートポインタとしてaddrootに登録します。

  1. ローカル変数領域: sp(スタックポインタ)からf->frame - sizeof(uintptr)の範囲。sizeof(uintptr)は戻りアドレスのサイズを考慮していると考えられます。これにより、スタックフレーム内のローカル変数領域がスキャン対象となります。
  2. 引数領域: sp + f->frameからf->argsの範囲。これは、スタックフレームの先頭(sp)からフレームサイズ分進んだ位置が引数領域の開始点であり、そこから引数のサイズ分がスキャン対象となります。

この関数により、GCはスタックフレーム内のポインタが存在する可能性のある正確な領域(ローカル変数と引数)のみをスキャンするようになります。

addstackroots 関数 (変更)

static void
addstackroots(G *gp)
{
	M *mp;
	Func *f;
	byte *sp, *pc;

	if(gp == g) {
		// Scanning our own stack: start at &gp.
		sp = (byte*)&gp;
		pc = runtime·getcallerpc(&gp);
	} else if((mp = gp->m) != nil && mp->helpgc) {
		// gchelper's stack is in active use and has no interesting pointers.
		return;
	} else if(gp->gcstack != (uintptr)nil) {
		// Scanning another goroutine that is about to enter or might
		// have just exited a system call. It may be executing code such
		// as schedlock and may have needed to start a new stack segment.
		// Use the stack segment and stack pointer at the time of
		// the system call instead, since that won't change underfoot.
		sp = (byte*)gp->gcsp;
		pc = gp->gcpc;
	} else {
		// Scanning another goroutine's stack.
		// The goroutine is usually asleep (the world is stopped).
		sp = (byte*)gp->sched.sp;
		pc = gp->sched.pc;
		if (pc == (byte*)runtime·goexit && gp->fnstart != nil) {
			// The goroutine has not started.  Its incoming
			// arguments are at the top of the stack and must
			// be scanned.  No other data on the stack.
			f = runtime·findfunc((uintptr)gp->fnstart->fn);
			if (f->args > 0)
				addroot((Obj){sp, f->args, 0});
			return;
		}
	}
	runtime·gentraceback(pc, sp, nil, gp, 0, nil, 0x7fffffff, addframeroots, nil);
}

この関数は、特定のゴルーチンgpのスタックをスキャンしてルートポインタを特定する役割を担います。

  • 以前はStktop構造体を使ってスタックセグメントを直接辿っていましたが、この変更により、スタックトレースを生成する汎用関数であるruntime·gentracebackに処理を委譲するようになりました。
  • runtime·gentracebackの最後の2つの引数に、新しく定義されたaddframeroots関数とnil(ユーザーデータ)を渡しています。これにより、gentracebackがスタックフレームを辿るたびにaddframerootsが呼び出され、各フレームのローカル変数と引数がルートとして登録されます。
  • システムコール中や、まだ開始されていないゴルーチン(runtime·goexitにいる場合)のスタック処理に関する特殊なケースも考慮されています。特に、まだ開始されていないゴルーチンの場合は、その引数のみをスキャンして終了します。

runtime·gentraceback 関数のシグネチャ変更

int32	runtime·gentraceback(byte*, byte*, byte*, G*, int32, uintptr*, int32,  void (*fn)(Func*, byte*, byte*, void*), void *arg);

この変更は、runtime·gentraceback関数が新しいコールバック関数ポインタfnと、そのコールバックに渡される任意の引数argを受け取るようになったことを示しています。これにより、gentracebackはスタックトレースの表示だけでなく、GCのような他の目的でスタックフレームを処理するための汎用的なメカニズムとして機能するようになりました。

traceback_arm.c および traceback_x86.c における runtime·gentraceback の実装変更

これらのファイルでは、runtime·gentracebackの内部ロジックが更新され、新しいfnコールバック引数が利用されるようになりました。

  • if(pcbuf != nil)のブロックに加えて、else if(fn != nil)のブロックが追加されました。これは、pcbufが指定されている場合はPCをバッファに格納し、fnが指定されている場合はコールバック関数を呼び出すという、異なる処理パスを可能にします。
  • runtime·goexitに到達した場合のブレーク条件は、スタックトレースがゴルーチンの開始点を超えてアンワインドされるのを防ぎ、正確なスタックの終端を保証します。

これらの変更により、Goのガベージコレクタはスタック上のポインタをより正確に識別できるようになり、GCの効率と信頼性が向上しました。これは、GoのGCが保守的なスキャンからより正確なスキャンへと進化する過程における重要な一歩です。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (当時の情報源を探す必要があります)
  • Goランタイムのソースコードリポジトリ
  • Goのスタック管理に関する設計ドキュメントや議論

参考にした情報源リンク

  • Goのコミット履歴 (特定のコミットを辿るために使用)
  • Go CL 7301062 (元のコードレビューと変更の詳細)
  • Goのガベージコレクションに関する一般的な知識 (GoのGCの進化に関する記事や論文)
  • スタックフレーム、スタックポインタ、プログラムカウンタなどのコンピュータサイエンスの基本概念
  • C言語の関数ポインタとコールバックの概念