[インデックス 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より大きい場合に、スタックのゼロ初期化を行うアセンブリ命令を挿入します。具体的には、以下の処理が行われます。
AMOVQ, D_CONST, 0, D_AX, 0
:AX
レジスタに0をロードします。これは、スタックをゼロで埋めるための値です。AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0
:CX
レジスタにゼロ初期化するワード数(stkzerosize
をポインタの幅で割った値)をロードします。これは、REP STOSQ
命令の繰り返し回数を指定します。ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0
:DI
レジスタに、ゼロ初期化を開始するスタック上のアドレスをロードします。D_SP
はスタックポインタ、frame-stkzerosize
はスタックフレーム内のオフセットを示します。AREP, D_NONE, 0, D_NONE, 0
:REP
プレフィックスを生成します。これは次の命令をCX
レジスタの値だけ繰り返すことを意味します。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, 0
とASTOSQ, 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, 0
とASTOSQ, 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