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

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

このコミットは、Goコンパイラ(cmd/gc、特にamd64アーキテクチャ向けの6g)とランタイムにおけるスタックのゼロ初期化に関する変更を導入しています。主な目的は、Goのガベージコレクタがより正確なスタックポインタ情報を利用できるように、「正確なスタックアカウンティング(precise stack accounting)」を可能にすることです。これは、GOEXPERIMENT=precisestackという実験的なフラグを有効にした場合にのみ効果を発揮するもので、将来的なGoのガベージコレクションの改善に向けた重要な一歩となります。

コミット

commit a81692e2650fce39bebd77224f4153a326460286
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jan 23 23:11:04 2014 -0500

    cmd/gc: add zeroing to enable precise stack accounting
    
    There is more zeroing than I would like right now -
    temporaries used for the new map and channel runtime
    calls need to be eliminated - but it will do for now.
    
    This CL only has an effect if you are building with
    
            GOEXPERIMENT=precisestack ./all.bash
    
    (or make.bash). It costs about 5% in the overall time
    spent in all.bash. That number will come down before
    we make it on by default, but this should be enough for
    Keith to try using the precise maps for copying stacks.
    
    amd64 only (and it's not really great generated code).
    
    TBR=khr, iant
    CC=golang-codereviews
    https://golang.org/cl/56430043

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

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

元コミット内容

cmd/gc: add zeroing to enable precise stack accounting

現在、望ましいよりも多くのゼロ初期化が行われている。新しいマップとチャネルのランタイム呼び出しに使用される一時変数は排除される必要があるが、当面はこれで十分だろう。

この変更は、GOEXPERIMENT=precisestack ./all.bash (または make.bash) でビルドした場合にのみ効果がある。all.bash全体の実行時間で約5%のコストがかかる。この数値はデフォルトで有効になる前に削減される予定だが、これはKeithがスタックコピーに正確なマップを使用してみるのに十分なはずだ。

amd64のみ(そして、生成されるコードはあまり良くない)。

変更の背景

Goのガベージコレクタ(GC)は、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。GCが正しく動作するためには、どのメモリ領域がポインタを含んでいるか、そしてそれらのポインタがどこを指しているかを正確に把握する必要があります。特に、関数呼び出しの際に使用されるスタックフレーム上の変数は、ポインタを含む可能性があり、GCがそれらを正確に識別することが重要です。

このコミットの背景には、Goのガベージコレクションをより効率的かつ正確にするための取り組みがあります。従来のGoのGCは、スタック上のポインタを識別する際に、一部の変数を「曖昧にライブ(ambiguously live)」と見なすことがありました。これは、コンパイラが変数の生存期間を正確に追跡できない場合に発生し、GCが誤ってポインタではない値をポインタとして扱ったり、逆にポインタである値を見落としたりするリスクがありました。このような曖昧さは、GCの正確性を損ない、不必要なメモリ保持や、最悪の場合、プログラムのクラッシュにつながる可能性がありました。

「正確なスタックアカウンティング(precise stack accounting)」は、この問題を解決し、スタック上のすべてのポインタをGCが正確に識別できるようにすることを目指しています。そのためには、スタック上のポインタではない領域、特に一時変数や未使用のメモリ領域が、GCがポインタと誤認するような値を含まないように、ゼロ初期化される必要があります。このコミットは、そのためのゼロ初期化処理をコンパイラに導入するものです。

また、コミットメッセージにある「Keithがスタックコピーに正確なマップを使用してみる」という記述は、Goのランタイムがゴルーチンスタックを移動(コピー)する際に、スタック上のポインタを正確に把握する必要があることを示唆しています。正確なスタックマップがあれば、スタックのコピーがより効率的かつ安全に行えるようになります。

前提知識の解説

ガベージコレクション (GC) とポインタスキャン

Goのガベージコレクタは、マーク&スイープ方式をベースにしています。GCは、プログラムがアクセス可能なすべてのオブジェクト(ポインタによって参照されているオブジェクト)を「ライブ」とマークし、それ以外の「デッド」なオブジェクトを解放します。このプロセスにおいて、スタック上のポインタを正確に識別し、それらが指すヒープ上のオブジェクトをマークすることは非常に重要です。

スタックフレームと変数

関数が呼び出されると、その関数専用のスタックフレームが作成されます。このスタックフレームには、関数のローカル変数、引数、戻りアドレスなどが格納されます。Goの変数は、値型(int, boolなど)と参照型(ポインタ、スライス、マップ、チャネルなど)に大別されます。GCは参照型の変数、特にポインタを追跡する必要があります。

曖昧にライブな変数 (Ambiguously Live Variables)

コンパイラが変数の生存期間を正確に判断できない場合、その変数は「曖昧にライブ」と見なされることがあります。これは、変数が実際に使用されなくなった後も、そのメモリ領域に古い値(ポインタのように見えるビットパターン)が残っている可能性があるためです。GCがこのような領域をスキャンすると、誤って無効なポインタを追跡しようとしたり、既に解放されたメモリを指すポインタをライブと誤認したりする可能性があります。

スタックのゼロ初期化 (Stack Zeroing)

スタックのゼロ初期化とは、関数が呼び出された際に、そのスタックフレーム内の未使用領域や、ポインタを含まないことが保証されている領域をゼロで埋める処理です。これにより、GCがスタックをスキャンする際に、古い値やランダムなビットパターンをポインタと誤認するリスクを排除し、GCの正確性を向上させることができます。

GOEXPERIMENT 環境変数

GOEXPERIMENTは、Goのコンパイラやランタイムの実験的な機能を有効にするための環境変数です。このコミットでは、GOEXPERIMENT=precisestackを設定することで、この新しいスタックゼロ初期化と正確なスタックアカウンティングのロジックが有効になります。これは、新機能がデフォルトで有効になる前に、その影響やパフォーマンスを評価するためのメカニズムです。

amd64 アーキテクチャ

このコミットはamd64アーキテクチャに特化しています。これは、コンパイラのバックエンド(cmd/6g)が特定のアーキテクチャのコード生成を担当するためです。スタックのレイアウトやレジスタの使用方法はアーキテクチャによって異なるため、このような低レベルの最適化は通常、特定のアーキテクチャに依存します。

技術的詳細

このコミットは、主にGoコンパイラのバックエンド(src/cmd/6g/ggen.c)と、ライブネス解析を行う部分(src/cmd/gc/plive.c)、そしてランタイムのGCスキャン部分(src/pkg/runtime/mgc0.c)に影響を与えます。

src/cmd/6g/ggen.c の変更

ggen.cは、amd64アーキテクチャ向けのGoコードをアセンブリに変換する役割を担っています。 このファイルでは、defframe関数が変更されています。defframeは、関数のプロローグ(関数が呼び出された直後に実行されるコード)を生成する際に、スタックフレームのセットアップを行う関数です。

追加されたコードは、stkzerosize(ゼロ初期化が必要なスタック領域のサイズ)が0より大きい場合に、スタックのゼロ初期化を行うアセンブリ命令を挿入します。具体的には、以下の処理が行われます。

  1. AMOVQ, D_CONST, 0, D_AX, 0: AXレジスタに0をロードします。これは、スタックをゼロで埋めるための値です。
  2. AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0: CXレジスタにゼロ初期化するワード数(stkzerosizeをポインタの幅で割った値)をロードします。これは、REP STOSQ命令の繰り返し回数を指定します。
  3. ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0: DIレジスタに、ゼロ初期化を開始するスタック上のアドレスをロードします。D_SPはスタックポインタ、frame-stkzerosizeはスタックフレーム内のオフセットを示します。
  4. AREP, D_NONE, 0, D_NONE, 0: REPプレフィックスを生成します。これは次の命令をCXレジスタの値だけ繰り返すことを意味します。
  5. ASTOSQ, D_NONE, 0, D_NONE, 0: STOSQ命令を生成します。これはAXレジスタの内容をDIレジスタが指すメモリ位置にストアし、DIレジスタをインクリメントします。REPプレフィックスと組み合わせることで、指定されたサイズのメモリ領域を効率的にゼロで埋めます。

また、appendppというヘルパー関数が追加されています。これは、アセンブリ命令(Prog構造体)を生成し、命令リストに連結するためのユーティリティ関数です。

clearfat関数内のgfatvardef(nl);の呼び出し位置が変更されています。これは、大きな構造体や配列のゼロ初期化に関連する処理で、スタックゼロ初期化のロジックと連携して、変数の定義と初期化の順序を調整している可能性があります。

src/cmd/gc/plive.c の変更

plive.cは、Goコンパイラのライブネス解析(変数がいつライブであるか、つまり使用されているかを判断するプロセス)を担当しています。 livenessepilogue関数が変更されています。この関数は、ライブネス解析の最終段階で、変数のライブネス情報を処理します。

変更の核心は、precisestack_enabledというフラグ(GOEXPERIMENT=precisestackが有効な場合に真となる)が導入されたことです。このフラグが真の場合、コンパイラは「曖昧にライブな変数」を特定し、それらにn->needzero = 1;を設定します。needzeroフラグは、その変数がゼロ初期化を必要とすることを示します。

以前のコードでは、debugliveが特定のレベルの場合に曖昧にライブな変数を診断するだけでしたが、新しいコードでは、precisestack_enabledの場合に、これらの変数を実際にゼロ初期化の対象としてマークするようになりました。これにより、GCがスタックをスキャンする際に、これらの曖昧な領域をポインタと誤認する可能性がなくなります。

bvcmp(livein, liveout) != 0から!bvisempty(liveout)への条件変更は、曖昧にライブな変数が存在するかどうかのチェックをより直接的に行っています。

src/pkg/runtime/mgc0.c の変更

mgc0.cは、Goランタイムのガベージコレクタの一部で、特にスタックのスキャンに関連する処理が含まれています。 scanframe関数が変更されています。この関数は、特定のスタックフレームをスキャンしてポインタを識別します。

変更点は、scanbitvector関数の第3引数がfalseからtrueに変更されたことです。 scanbitvectorは、ビットマップ(スタックマップ)に基づいてメモリ領域をスキャンし、ポインタを識別する関数です。第3引数は通常、その領域が「正確な(precise)」であるかどうかを示すフラグです。trueに設定することで、このスタックフレームの引数領域が正確なポインタ情報を持っていることをGCに伝えます。これにより、GCはより信頼性の高い情報に基づいてポインタをスキャンできるようになります。

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

src/cmd/6g/ggen.c

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -9,10 +9,13 @@
 #include "gg.h"
 #include "opt.h"
 
+static Prog *appendpp(Prog*, int, int, vlong, int, vlong);
+
 void
 defframe(Prog *ptxt)
 {
 	uint32 frame;
+	Prog *p;
 
 	// fill in argument size
 	ptxt->to.offset = rnd(curfn->type->argwid, widthptr);
@@ -21,6 +24,35 @@ defframe(Prog *ptxt)
 	ptxt->to.offset <<= 32;
 	frame = rnd(stksize+maxarg, widthptr);
 	ptxt->to.offset |= frame;
+	
+	// insert code to contain ambiguously live variables
+	// so that garbage collector only sees initialized values
+	// when it looks for pointers.
+	p = ptxt;
+	if(stkzerosize > 0) {
+		p = appendpp(p, AMOVQ, D_CONST, 0, D_AX, 0);	
+		p = appendpp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);	
+		p = appendpp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);	
+		p = appendpp(p, AREP, D_NONE, 0, D_NONE, 0);	
+		appendpp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);	
+	}
+}
+
+static Prog*	
+appendpp(Prog *p, int as, int ftype, vlong foffset, int ttype, vlong toffset)	
+{
+	Prog *q;
+	q = mal(sizeof(*q));	
+	clearp(q);	
+	q->as = as;	
+	q->lineno = p->lineno;	
+	q->from.type = ftype;	
+	q->from.offset = foffset;	
+	q->to.type = ttype;	
+	q->to.offset = toffset;	
+	q->link = p->link;	
+	p->link = q;	
+	return q;	
 }
 
 // Sweep the prog list to mark any used nodes.
@@ -990,14 +1022,13 @@ clearfat(Node *nl)
 	if(debug['g'])
 		dump("\nclearfat", nl);
 
+	gfatvardef(nl);
 
 	w = nl->type->width;
 	// Avoid taking the address for simple enough types.
 	if(componentgen(N, nl))
 		return;
 
-	gfatvardef(nl);
-
 	c = w % 8;	// bytes
 	q = w / 8;	// quads
 

src/cmd/gc/plive.c

--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -1498,25 +1498,29 @@ livenessepilogue(Liveness *lv)
 			bvor(all, all, avarinit);
 
 			if(issafepoint(p)) {
-				if(debuglive >= 3) {
-					// Diagnose ambiguously live variables (any &^ all).
-					// livein and liveout are dead here and used as temporaries.
+				// Annotate ambiguously live variables so that they can
+				// be zeroed at function entry.
+				// livein and liveout are dead here and used as temporaries.
+				// For now, only enabled when using GOEXPERIMENT=precisestack
+				// during make.bash / all.bash.
+				if(precisestack_enabled) {
 					bvresetall(livein);
 					bvandnot(liveout, any, all);
-					if(bvcmp(livein, liveout) != 0) {
+					if(!bvisempty(liveout)) {
 						for(pos = 0; pos < liveout->n; pos++) {
-							if(bvget(liveout, pos)) {
-								n = *(Node**)arrayget(lv->vars, pos);
-								if(!n->diag && strncmp(n->sym->name, "autotmp_", 8) != 0) {
-									n->diag = 1;
+							bvset(all, pos); // silence future warnings in this block
+							if(!bvget(liveout, pos))
+								continue;
+							n = *(Node**)arrayget(lv->vars, pos);
+							if(!n->needzero) {
+								n->needzero = 1;
+								if(debuglive >= 3)
 									warnl(p->lineno, "%N: %lN is ambiguously live", curfn->nname, n);
-								}
 							}
-							bvset(all, pos); // silence future warnings in this block
 						}
 					}
 				}
-
+	
 				// Allocate a bit vector for each class and facet of
 				// value we are tracking.
 	

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1505,7 +1505,7 @@ scanframe(Stkframe *frame, void *wbufp)
 	stackmap = runtime·funcdata(f, FUNCDATA_ArgsPointerMaps);
 	if(stackmap != nil) {
 		bv = stackmapdata(stackmap, pcdata);
-		scanbitvector(frame->argp, bv, false, wbufp);
+		scanbitvector(frame->argp, bv, true, wbufp);
 	} else
 		enqueue1(wbufp, (Obj){frame->argp, frame->arglen, 0});
 }

コアとなるコードの解説

src/cmd/6g/ggen.c の変更点

  • defframe 関数へのゼロ初期化コードの追加:
    • stkzerosize は、スタックフレーム内でゼロ初期化が必要なバイト数を示します。これは、ライブネス解析によって「曖昧にライブ」と判断された変数や、GCがポインタと誤認する可能性のある一時変数などの領域の合計サイズです。
    • AMOVQ, D_CONST, 0, D_AX, 0: AXレジスタに0をセットします。これは、メモリをゼロで埋めるための値です。
    • AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0: CXレジスタに、ゼロ初期化するQWORD(8バイト)の数をセットします。widthptrはポインタのサイズ(amd64では8バイト)です。REP STOSQ命令はこのCXレジスタの値を繰り返し回数として使用します。
    • ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0: DIレジスタに、ゼロ初期化を開始するスタック上のアドレスを計算してセットします。D_SPはスタックポインタ、frameはスタックフレームの合計サイズ、stkzerosizeはゼロ初期化する領域のサイズです。これにより、スタックフレームの適切な位置からゼロ初期化が開始されます。
    • AREP, D_NONE, 0, D_NONE, 0ASTOSQ, D_NONE, 0, D_NONE, 0: これらはx86-64アセンブリのREP STOSQ命令を生成します。REPプレフィックスは、STOSQ命令をCXレジスタが示す回数だけ繰り返すことを指示します。STOSQ命令は、AXレジスタの内容(この場合は0)をDIレジスタが指すメモリ位置にストアし、DIレジスタを自動的にインクリメントします。これにより、stkzerosizeで指定されたスタック領域が効率的にゼロで埋められます。
  • appendpp ヘルパー関数の追加:
    • これは、新しいアセンブリ命令(Prog構造体)を作成し、既存の命令リストの末尾に連結するためのユーティリティ関数です。これにより、コード生成ロジックがより整理されます。
  • clearfat 関数内の gfatvardef(nl); の移動:
    • この変更は、大きな構造体や配列のゼロ初期化に関連するものです。gfatvardefは、変数の定義と初期化に関連する処理を行うと考えられます。その呼び出し位置を変更することで、スタックゼロ初期化のロジックと連携し、変数の初期化がGCの正確なスタックアカウンティングの要件を満たすように調整されている可能性があります。

src/cmd/gc/plive.c の変更点

  • livenessepilogue 関数における precisestack_enabled の導入:
    • precisestack_enabled は、GOEXPERIMENT=precisestackが有効な場合に真となるフラグです。このフラグが導入されたことで、実験的な正確なスタックアカウンティングのロジックが条件付きで実行されるようになります。
    • このブロック内で、bvresetall(livein);bvandnot(liveout, any, all); を使用して、「曖昧にライブな変数」を特定します。anyはGCがポインタと見なす可能性のあるすべての変数を表し、allはコンパイラが確実にライブであると判断した変数を表します。any &^ allは、GCがポインタと見なす可能性があるが、コンパイラがライブであると断定できない変数(すなわち、曖昧にライブな変数)を抽出します。
    • if(!bvisempty(liveout)) は、曖昧にライブな変数が存在するかどうかをチェックします。
    • ループ内で、特定された曖昧にライブな変数nに対して n->needzero = 1; を設定します。このneedzeroフラグは、その変数がスタックゼロ初期化の対象となることをコンパイラのバックエンド(ggen.c)に伝えます。
    • bvset(all, pos); は、将来の警告を抑制するために、これらの変数を「ライブ」としてマークします。これは、デバッグ目的で曖昧な変数を診断する際の副作用を管理するためと考えられます。

src/pkg/runtime/mgc0.c の変更点

  • scanframe 関数における scanbitvector の引数変更:
    • scanframe は、ランタイムのGCがスタックフレームをスキャンする際に呼び出される関数です。
    • scanbitvector(frame->argp, bv, false, wbufp);scanbitvector(frame->argp, bv, true, wbufp); に変更されました。
    • scanbitvector の第3引数は、スキャン対象のメモリ領域が「正確な(precise)」ポインタ情報を持っているかどうかを示します。falseは「曖昧な(ambiguous)」スキャンを意味し、GCはより保守的にポインタを推測します。trueは「正確な」スキャンを意味し、GCはビットマップ(bv)に示された情報に基づいて、ポインタを正確に識別します。
    • この変更により、ランタイムのGCは、コンパイラが生成した正確なスタックマップ情報(FUNCDATA_ArgsPointerMapsによって提供される)を信頼し、スタックフレームの引数領域をより正確にスキャンできるようになります。これは、スタックゼロ初期化と連携して、GCの正確性を向上させるための重要なステップです。

これらの変更は、Goのガベージコレクタがスタック上のポインタをより正確に識別できるようにするための基盤を構築しています。スタックのゼロ初期化により、GCがポインタと誤認する可能性のある「ノイズ」が除去され、ライブネス解析によって生成された正確なスタックマップ情報がランタイムで活用されることで、GCの効率と信頼性が向上します。

関連リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事(当時のもの)
  • Goコンパイラの内部構造に関する資料
  • amd64アセンブリ言語の命令セットに関する資料

参考にした情報源リンク

  • Go言語の公式ドキュメント (golang.org)
  • Goのソースコードリポジトリ (github.com/golang/go)
  • Goのガベージコレクションに関する技術ブログや論文
  • x86-64 Instruction Set ReferenceI have provided a detailed explanation of the commit. I will now output it to standard output.
# [インデックス 18347] ファイルの概要

このコミットは、Goコンパイラ(`cmd/gc`、特に`amd64`アーキテクチャ向けの`6g`)とランタイムにおけるスタックのゼロ初期化に関する変更を導入しています。主な目的は、Goのガベージコレクタがより正確なスタックポインタ情報を利用できるように、「正確なスタックアカウンティング(precise stack accounting)」を可能にすることです。これは、`GOEXPERIMENT=precisestack`という実験的なフラグを有効にした場合にのみ効果を発揮するもので、将来的なGoのガベージコレクションの改善に向けた重要な一歩となります。

## コミット

commit a81692e2650fce39bebd77224f4153a326460286 Author: Russ Cox rsc@golang.org Date: Thu Jan 23 23:11:04 2014 -0500

cmd/gc: add zeroing to enable precise stack accounting

There is more zeroing than I would like right now -
temporaries used for the new map and channel runtime
calls need to be eliminated - but it will do for now.

This CL only has an effect if you are building with

        GOEXPERIMENT=precisestack ./all.bash

(or make.bash). It costs about 5% in the overall time
spent in all.bash. That number will come down before
we make it on by default, but this should be enough for
Keith to try using the precise maps for copying stacks.

amd64 only (and it's not really great generated code).

TBR=khr, iant
CC=golang-codereviews
https://golang.org/cl/56430043

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

[https://github.com/golang/go/commit/a81692e2650fce39bebd77224f4153a326460286](https://github.com/golang/go/commit/a81692e2650fce39bebd77224f4153a326460286)

## 元コミット内容

`cmd/gc: add zeroing to enable precise stack accounting`

現在、望ましいよりも多くのゼロ初期化が行われている。新しいマップとチャネルのランタイム呼び出しに使用される一時変数は排除される必要があるが、当面はこれで十分だろう。

この変更は、`GOEXPERIMENT=precisestack ./all.bash` (または `make.bash`) でビルドした場合にのみ効果がある。`all.bash`全体の実行時間で約5%のコストがかかる。この数値はデフォルトで有効になる前に削減される予定だが、これはKeithがスタックコピーに正確なマップを使用してみるのに十分なはずだ。

`amd64`のみ(そして、生成されるコードはあまり良くない)。

## 変更の背景

Goのガベージコレクタ(GC)は、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。GCが正しく動作するためには、どのメモリ領域がポインタを含んでいるか、そしてそれらのポインタがどこを指しているかを正確に把握する必要があります。特に、関数呼び出しの際に使用されるスタックフレーム上の変数は、ポインタを含む可能性があり、GCがそれらを正確に識別することが重要です。

このコミットの背景には、Goのガベージコレクションをより効率的かつ正確にするための取り組みがあります。従来のGoのGCは、スタック上のポインタを識別する際に、一部の変数を「曖昧にライブ(ambiguously live)」と見なすことがありました。これは、コンパイラが変数の生存期間を正確に追跡できない場合に発生し、GCが誤ってポインタではない値をポインタとして扱ったり、逆にポインタである値を見落としたりするリスクがありました。このような曖昧さは、GCの正確性を損ない、不必要なメモリ保持や、最悪の場合、プログラムのクラッシュにつながる可能性がありました。

「正確なスタックアカウンティング(precise stack accounting)」は、この問題を解決し、スタック上のすべてのポインタをGCが正確に識別できるようにすることを目指しています。そのためには、スタック上のポインタではない領域、特に一時変数や未使用のメモリ領域が、GCがポインタと誤認するような値を含まないように、ゼロ初期化される必要があります。このコミットは、そのためのゼロ初期化処理をコンパイラに導入するものです。

また、コミットメッセージにある「Keithがスタックコピーに正確なマップを使用してみる」という記述は、Goのランタイムがゴルーチンスタックを移動(コピー)する際に、スタック上のポインタを正確に把握する必要があることを示唆しています。正確なスタックマップがあれば、スタックのコピーがより効率的かつ安全に行えるようになります。

## 前提知識の解説

### ガベージコレクション (GC) とポインタスキャン

Goのガベージコレクタは、マーク&スイープ方式をベースにしています。GCは、プログラムがアクセス可能なすべてのオブジェクト(ポインタによって参照されているオブジェクト)を「ライブ」とマークし、それ以外の「デッド」なオブジェクトを解放します。このプロセスにおいて、スタック上のポインタを正確に識別し、それらが指すヒープ上のオブジェクトをマークすることは非常に重要です。

### スタックフレームと変数

関数が呼び出されると、その関数専用のスタックフレームが作成されます。このスタックフレームには、関数のローカル変数、引数、戻りアドレスなどが格納されます。Goの変数は、値型(int, boolなど)と参照型(ポインタ、スライス、マップ、チャネルなど)に大別されます。GCは参照型の変数、特にポインタを追跡する必要があります。

### 曖昧にライブな変数 (Ambiguously Live Variables)

コンパイラが変数の生存期間を正確に判断できない場合、その変数は「曖昧にライブ」と見なされることがあります。これは、変数が実際に使用されなくなった後も、そのメモリ領域に古い値(ポインタのように見えるビットパターン)が残っている可能性があるためです。GCがこのような領域をスキャンすると、誤って無効なポインタを追跡しようとしたり、既に解放されたメモリを指すポインタをライブと誤認したりする可能性があります。

### スタックのゼロ初期化 (Stack Zeroing)

スタックのゼロ初期化とは、関数が呼び出された際に、そのスタックフレーム内の未使用領域や、ポインタを含まないことが保証されている領域をゼロで埋める処理です。これにより、GCがスタックをスキャンする際に、古い値やランダムなビットパターンをポインタと誤認するリスクを排除し、GCの正確性を向上させることができます。

### `GOEXPERIMENT` 環境変数

`GOEXPERIMENT`は、Goのコンパイラやランタイムの実験的な機能を有効にするための環境変数です。このコミットでは、`GOEXPERIMENT=precisestack`を設定することで、この新しいスタックゼロ初期化と正確なスタックアカウンティングのロジックが有効になります。これは、新機能がデフォルトで有効になる前に、その影響やパフォーマンスを評価するためのメカニズムです。

### `amd64` アーキテクチャ

このコミットは`amd64`アーキテクチャに特化しています。これは、コンパイラのバックエンド(`cmd/6g`)が特定のアーキテクチャのコード生成を担当するためです。スタックのレイアウトやレジスタの使用方法はアーキテクチャによって異なるため、このような低レベルの最適化は通常、特定のアーキテクチャに依存します。

## 技術的詳細

このコミットは、主にGoコンパイラのバックエンド(`src/cmd/6g/ggen.c`)と、ライブネス解析を行う部分(`src/cmd/gc/plive.c`)、そしてランタイムのGCスキャン部分(`src/pkg/runtime/mgc0.c`)に影響を与えます。

### `src/cmd/6g/ggen.c` の変更

`ggen.c`は、`amd64`アーキテクチャ向けのGoコードをアセンブリに変換する役割を担っています。
このファイルでは、`defframe`関数が変更されています。`defframe`は、関数のプロローグ(関数が呼び出された直後に実行されるコード)を生成する際に、スタックフレームのセットアップを行う関数です。

追加されたコードは、`stkzerosize`(ゼロ初期化が必要なスタック領域のサイズ)が0より大きい場合に、スタックのゼロ初期化を行うアセンブリ命令を挿入します。具体的には、以下の処理が行われます。

1.  `AMOVQ, D_CONST, 0, D_AX, 0`: `AX`レジスタに0をロードします。これは、スタックをゼロで埋めるための値です。
2.  `AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0`: `CX`レジスタにゼロ初期化するワード数(`stkzerosize`をポインタの幅で割った値)をロードします。これは、`REP STOSQ`命令の繰り返し回数を指定します。
3.  `ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0`: `DI`レジスタに、ゼロ初期化を開始するスタック上のアドレスをロードします。`D_SP`はスタックポインタ、`frame-stkzerosize`はスタックフレーム内のオフセットを示します。
4.  `AREP, D_NONE, 0, D_NONE, 0`: `REP`プレフィックスを生成します。これは次の命令を`CX`レジスタの値だけ繰り返すことを意味します。
5.  `ASTOSQ, D_NONE, 0, D_NONE, 0`: `STOSQ`命令を生成します。これは`AX`レジスタの内容を`DI`レジスタが指すメモリ位置にストアし、`DI`レジスタをインクリメントします。`REP`プレフィックスと組み合わせることで、指定されたサイズのメモリ領域を効率的にゼロで埋めます。

また、`appendpp`というヘルパー関数が追加されています。これは、アセンブリ命令(`Prog`構造体)を生成し、命令リストに連結するためのユーティリティ関数です。

`clearfat`関数内の`gfatvardef(nl);`の呼び出し位置が変更されています。これは、大きな構造体や配列のゼロ初期化に関連する処理で、スタックゼロ初期化のロジックと連携して、変数の定義と初期化の順序を調整している可能性があります。

### `src/cmd/gc/plive.c` の変更

`plive.c`は、Goコンパイラのライブネス解析(変数がいつライブであるか、つまり使用されているかを判断するプロセス)を担当しています。
`livenessepilogue`関数が変更されています。この関数は、ライブネス解析の最終段階で、変数のライブネス情報を処理します。

変更の核心は、`precisestack_enabled`というフラグ(`GOEXPERIMENT=precisestack`が有効な場合に真となる)が導入されたことです。このフラグが真の場合、コンパイラは「曖昧にライブな変数」を特定し、それらに`n->needzero = 1;`を設定します。`needzero`フラグは、その変数がゼロ初期化を必要とすることを示します。

以前のコードでは、`debuglive`が特定のレベルの場合に曖昧にライブな変数を診断するだけでしたが、新しいコードでは、`precisestack_enabled`の場合に、これらの変数を実際にゼロ初期化の対象としてマークするようになりました。これにより、GCがスタックをスキャンする際に、これらの曖昧な領域をポインタと誤認する可能性がなくなります。

`bvcmp(livein, liveout) != 0`から`!bvisempty(liveout)`への条件変更は、曖昧にライブな変数が存在するかどうかのチェックをより直接的に行っています。

### `src/pkg/runtime/mgc0.c` の変更

`mgc0.c`は、Goランタイムのガベージコレクタの一部で、特にスタックのスキャンに関連する処理が含まれています。
`scanframe`関数が変更されています。この関数は、特定のスタックフレームをスキャンしてポインタを識別します。

変更点は、`scanbitvector`関数の第3引数が`false`から`true`に変更されたことです。
`scanbitvector`は、ビットマップ(スタックマップ)に基づいてメモリ領域をスキャンし、ポインタを識別する関数です。第3引数は通常、その領域が「正確な(precise)」であるかどうかを示すフラグです。`true`に設定することで、このスタックフレームの引数領域が正確なポインタ情報を持っていることをGCに伝えます。これにより、GCはより信頼性の高い情報に基づいてポインタをスキャンできるようになります。

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

### `src/cmd/6g/ggen.c`

```diff
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -9,10 +9,13 @@
 #include "gg.h"
 #include "opt.h"
 
+static Prog *appendpp(Prog*, int, int, vlong, int, vlong);
+
 void
 defframe(Prog *ptxt)
 {
 	uint32 frame;
+	Prog *p;
 
 	// fill in argument size
 	ptxt->to.offset = rnd(curfn->type->argwid, widthptr);
@@ -21,6 +24,35 @@ defframe(Prog *ptxt)
 	ptxt->to.offset <<= 32;
 	frame = rnd(stksize+maxarg, widthptr);
 	ptxt->to.offset |= frame;
+	
+	// insert code to contain ambiguously live variables
+	// so that garbage collector only sees initialized values
+	// when it looks for pointers.
+	p = ptxt;
+	if(stkzerosize > 0) {
+		p = appendpp(p, AMOVQ, D_CONST, 0, D_AX, 0);	
+		p = appendpp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);	
+		p = appendpp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);	
+		p = appendpp(p, AREP, D_NONE, 0, D_NONE, 0);	
+		appendpp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);	
+	}
+}
+
+static Prog*	
+appendpp(Prog *p, int as, int ftype, vlong foffset, int ttype, vlong toffset)	
+{
+	Prog *q;
+	q = mal(sizeof(*q));	
+	clearp(q);	
+	q->as = as;	
+	q->lineno = p->lineno;	
+q->from.type = ftype;	
+	q->from.offset = foffset;	
+	q->to.type = ttype;	
+	q->to.offset = toffset;	
+	q->link = p->link;	
+	p->link = q;	
+	return q;	
 }
 
 // Sweep the prog list to mark any used nodes.
@@ -990,14 +1022,13 @@ clearfat(Node *nl)
 	if(debug['g'])
 		dump("\nclearfat", nl);
 
+	gfatvardef(nl);
 
 	w = nl->type->width;
 	// Avoid taking the address for simple enough types.
 	if(componentgen(N, nl))
 		return;
 
-	gfatvardef(nl);
-
 	c = w % 8;	// bytes
 	q = w / 8;	// quads
 

src/cmd/gc/plive.c

--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -1498,25 +1498,29 @@ livenessepilogue(Liveness *lv)
 			bvor(all, all, avarinit);
 
 			if(issafepoint(p)) {
-				if(debuglive >= 3) {
-					// Diagnose ambiguously live variables (any &^ all).
-					// livein and liveout are dead here and used as temporaries.
+				// Annotate ambiguously live variables so that they can
+				// be zeroed at function entry.
+				// livein and liveout are dead here and used as temporaries.
+				// For now, only enabled when using GOEXPERIMENT=precisestack
+				// during make.bash / all.bash.
+				if(precisestack_enabled) {
 					bvresetall(livein);
 					bvandnot(liveout, any, all);
-					if(bvcmp(livein, liveout) != 0) {
+					if(!bvisempty(liveout)) {
 						for(pos = 0; pos < liveout->n; pos++) {
-							if(bvget(liveout, pos)) {
-								n = *(Node**)arrayget(lv->vars, pos);
-								if(!n->diag && strncmp(n->sym->name, "autotmp_", 8) != 0) {
-									n->diag = 1;
+							bvset(all, pos); // silence future warnings in this block
+							if(!bvget(liveout, pos))
+								continue;
+							n = *(Node**)arrayget(lv->vars, pos);
+							if(!n->needzero) {
+								n->needzero = 1;
+								if(debuglive >= 3)
 									warnl(p->lineno, "%N: %lN is ambiguously live", curfn->nname, n);
-								}
 							}
-							bvset(all, pos); // silence future warnings in this block
 						}
 					}
 				}
-
+	
 				// Allocate a bit vector for each class and facet of
 				// value we are tracking.
 	

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1505,7 +1505,7 @@ scanframe(Stkframe *frame, void *wbufp)
 	stackmap = runtime·funcdata(f, FUNCDATA_ArgsPointerMaps);
 	if(stackmap != nil) {
 		bv = stackmapdata(stackmap, pcdata);
-		scanbitvector(frame->argp, bv, false, wbufp);
+		scanbitvector(frame->argp, bv, true, wbufp);
 	} else
 		enqueue1(wbufp, (Obj){frame->argp, frame->arglen, 0});
 }

コアとなるコードの解説

src/cmd/6g/ggen.c の変更点

  • defframe 関数へのゼロ初期化コードの追加:
    • stkzerosize は、スタックフレーム内でゼロ初期化が必要なバイト数を示します。これは、ライブネス解析によって「曖昧にライブ」と判断された変数や、GCがポインタと誤認する可能性のある一時変数などの領域の合計サイズです。
    • AMOVQ, D_CONST, 0, D_AX, 0: AXレジスタに0をセットします。これは、メモリをゼロで埋めるための値です。
    • AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0: CXレジスタに、ゼロ初期化するQWORD(8バイト)の数をセットします。widthptrはポインタのサイズ(amd64では8バイト)です。REP STOSQ命令はこのCXレジスタの値を繰り返し回数として使用します。
    • ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0: DIレジスタに、ゼロ初期化を開始するスタック上のアドレスを計算してセットします。D_SPはスタックポインタ、frameはスタックフレームの合計サイズ、stkzerosizeはゼロ初期化する領域のサイズです。これにより、スタックフレームの適切な位置からゼロ初期化が開始されます。
    • AREP, D_NONE, 0, D_NONE, 0ASTOSQ, D_NONE, 0, D_NONE, 0: これらはx86-64アセンブリのREP STOSQ命令を生成します。REPプレフィックスは、STOSQ命令をCXレジスタが示す回数だけ繰り返すことを指示します。STOSQ命令は、AXレジスタの内容(この場合は0)をDIレジスタが指すメモリ位置にストアし、DIレジスタを自動的にインクリメントします。これにより、stkzerosizeで指定されたスタック領域が効率的にゼロで埋められます。
  • appendpp ヘルパー関数の追加:
    • これは、新しいアセンブリ命令(Prog構造体)を作成し、既存の命令リストの末尾に連結するためのユーティリティ関数です。これにより、コード生成ロジックがより整理されます。
  • clearfat 関数内の gfatvardef(nl); の移動:
    • この変更は、大きな構造体や配列のゼロ初期化に関連するものです。gfatvardefは、変数の定義と初期化に関連する処理を行うと考えられます。その呼び出し位置を変更することで、スタックゼロ初期化のロジックと連携し、変数の初期化がGCの正確なスタックアカウンティングの要件を満たすように調整されている可能性があります。

src/cmd/gc/plive.c の変更点

  • livenessepilogue 関数における precisestack_enabled の導入:
    • precisestack_enabled は、GOEXPERIMENT=precisestackが有効な場合に真となるフラグです。このフラグが導入されたことで、実験的な正確なスタックアカウンティングのロジックが条件付きで実行されるようになります。
    • このブロック内で、bvresetall(livein);bvandnot(liveout, any, all); を使用して、「曖昧にライブな変数」を特定します。anyはGCがポインタと見なす可能性のあるすべての変数を表し、allはコンパイラが確実にライブであると判断した変数を表します。any &^ allは、GCがポインタと見なす可能性があるが、コンパイラがライブであると断定できない変数(すなわち、曖昧にライブな変数)を抽出します。
    • if(!bvisempty(liveout)) は、曖昧にライブな変数が存在するかどうかをチェックします。
    • ループ内で、特定された曖昧にライブな変数nに対して n->needzero = 1; を設定します。このneedzeroフラグは、その変数がスタックゼロ初期化の対象となることをコンパイラのバックエンド(ggen.c)に伝えます。
    • bvset(all, pos); は、将来の警告を抑制するために、これらの変数を「ライブ」としてマークします。これは、デバッグ目的で曖昧な変数を診断する際の副作用を管理するためと考えられます。

src/pkg/runtime/mgc0.c の変更点

  • scanframe 関数における scanbitvector の引数変更:
    • scanframe は、ランタイムのGCがスタックフレームをスキャンする際に呼び出される関数です。
    • scanbitvector(frame->argp, bv, false, wbufp);scanbitvector(frame->argp, bv, true, wbufp); に変更されました。
    • scanbitvector の第3引数は、スキャン対象のメモリ領域が「正確な(precise)」ポインタ情報を持っているかどうかを示します。falseは「曖昧な(ambiguous)」スキャンを意味し、GCはより保守的にポインタを推測します。trueは「正確な」スキャンを意味し、GCはビットマップ(bv)に示された情報に基づいて、ポインタを正確に識別します。
    • この変更により、ランタイムのGCは、コンパイラが生成した正確なスタックマップ情報(FUNCDATA_ArgsPointerMapsによって提供される)を信頼し、スタックフレームの引数領域をより正確にスキャンできるようになります。これは、スタックゼロ初期化と連携して、GCの正確性を向上させるための重要なステップです。

これらの変更は、Goのガベージコレクタがスタック上のポインタをより正確に識別できるようにするための基盤を構築しています。スタックのゼロ初期化により、GCがポインタと誤認する可能性のある「ノイズ」が除去され、ライブネス解析によって生成された正確なスタックマップ情報がランタイムで活用されることで、GCの効率と信頼性が向上します。

関連リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事(当時のもの)
  • Goコンパイラの内部構造に関する資料
  • amd64アセンブリ言語の命令セットに関する資料

参考にした情報源リンク

  • Go言語の公式ドキュメント (golang.org)
  • Goのソースコードリポジトリ (github.com/golang/go)
  • Goのガベージコレクションに関する技術ブログや論文
  • x86-64 Instruction Set Reference