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

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

このコミットは、Goコンパイラ(cmd/gc)において、スタックフレームのゼロ初期化の挙動を最適化するものです。具体的には、スタックフレーム内のポインタを含む領域(stkptrsize)と、実際にゼロ初期化が必要な領域(stkzerosize)を分離することで、ガベージコレクション(GC)の効率を向上させ、不要なゼロ初期化を削減します。これにより、関数呼び出し時のオーバーヘッドが軽減されます。

コミット

commit 3b4d792606bf6de962bed73e79c261d7ddd7266c
Author: Russ Cox <rsc@golang.org>
Date:   Fri Aug 16 21:45:59 2013 -0400

    cmd/gc: separate "has pointers" from "needs zeroing" in stack frame
    
    When the new call site-specific frame bitmaps are available,
    we can cut the zeroing to just those values that need it due
    to scope escaping.
    
    R=cshapiro, cshapiro
    CC=golang-dev
    https://golang.org/cl/13045043

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

https://github.com/golang/go/commit/3b4d792606bf6de962bed73e79c261d7ddd7266c

元コミット内容

cmd/gc: separate "has pointers" from "needs zeroing" in stack frame

このコミットは、Goコンパイラ(cmd/gc)において、スタックフレーム内の「ポインタを持つ領域」と「ゼロ初期化が必要な領域」を分離します。新しい呼び出しサイト固有のフレームビットマップが利用可能になった際、スコープエスケープによってゼロ初期化が必要な値のみにゼロ初期化を限定できるようになります。

変更の背景

Goのガベージコレクタは、スタック上のポインタを正確に識別し、ヒープ上のオブジェクトへの参照を追跡する必要があります。そのため、関数が呼び出される際、そのスタックフレームに割り当てられるメモリ領域は、GCが誤ったポインタを読み取ってクラッシュしたり、メモリリークを引き起こしたりしないように、適切に初期化される必要があります。特に、ポインタを含む可能性のある領域は、GCが安全にスキャンできるようにゼロ初期化されるのが一般的でした。

しかし、すべてのポインタを含む領域が常にゼロ初期化を必要とするわけではありません。例えば、ある変数がスタックに割り当てられたものの、その変数がヒープにエスケープしない(つまり、関数スコープ外から参照されない)場合、その変数の初期値がゼロでなくても、GCがその領域をスキャンする際に問題になることはありません。これまでの実装では、スタックフレーム内のポインタを含むすべての領域(stkptrsize)がゼロ初期化の対象となっていました。これは安全ではありますが、不要なゼロ初期化処理が発生し、特に大きなスタックフレームを持つ関数ではパフォーマンスのオーバーヘッドとなっていました。

このコミットの背景には、「新しい呼び出しサイト固有のフレームビットマップ」の導入があります。これは、各関数呼び出しサイトでスタックフレームのどの部分にポインタが含まれているかをより正確に追跡するためのメカニズムです。このより詳細な情報が利用可能になることで、コンパイラは「ポインタを持つ領域」と「ゼロ初期化が本当に必要な領域」を区別できるようになり、ゼロ初期化の範囲を最小限に抑えることが可能になります。これにより、GCの効率が向上し、ランタイムのパフォーマンスが改善されることが期待されます。

前提知識の解説

1. スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数に必要なローカル変数、引数、戻りアドレスなどを格納するためにメモリ上に確保される領域です。この領域はスタック上に積まれ、関数が終了すると解放されます。Goのスタックフレームは、ガベージコレクタがポインタを正確に識別できるように、ポインタ情報(スタックマップ)を持っています。

2. ポインタ (Pointer)

メモリ上の特定のアドレスを指し示す変数です。Goのガベージコレクタは、ポインタを追跡して到達可能なオブジェクトを特定し、到達不能なオブジェクトを解放します。スタックフレーム内のポインタは、ヒープ上のオブジェクトへの参照を持つことがあり、GCにとって重要な情報源となります。

3. メモリのゼロ初期化 (Memory Zeroing)

メモリ領域をすべてゼロで埋める処理です。Goでは、新しいメモリが割り当てられた際に、その内容が不定であることによる潜在的なバグやセキュリティリスクを防ぐために、ゼロ初期化が行われることがあります。特に、ポインタを含む可能性のあるメモリ領域は、GCが誤った値をポインタとして解釈しないように、ゼロ初期化されることが重要です。

4. ガベージコレクション (Garbage Collection, GC)

プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不能な)ものを自動的に解放する仕組みです。GoのGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作します。GCはスタックをスキャンしてポインタを識別し、ヒープ上のオブジェクトの到達可能性を判断します。

5. エスケープ解析 (Escape Analysis)

コンパイラが行う最適化の一つで、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。変数が関数スコープ外から参照される可能性がある場合(例えば、関数の戻り値として返される、グローバル変数に代入されるなど)、その変数は「エスケープする」と判断され、スタックではなくヒープに割り当てられます。エスケープしない変数はスタックに割り当てられ、関数終了時に自動的に解放されるため、GCの負担を軽減できます。

6. stkptrsizestkzerosize

  • stkptrsize: スタックフレームの先頭から、ポインタを含む可能性のある領域の終わりまでのサイズを示します。これまでの実装では、この領域全体がゼロ初期化の対象でした。
  • stkzerosize: このコミットで導入された新しい概念で、スタックフレームの先頭から、実際にゼロ初期化が必要な領域の終わりまでのサイズを示します。エスケープ解析の結果などに基づいて、stkptrsize よりも小さい値になる可能性があります。

7. cmd/gc (Go Compiler)

Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。スタックフレームのレイアウト、メモリ割り当て、GCのためのメタデータ生成など、ランタイムの低レベルな挙動に深く関与しています。

技術的詳細

このコミットの核心は、スタックフレームのゼロ初期化の粒度を細かくすることにあります。これまでは、スタックフレーム内でポインタを含む可能性のあるすべての領域(stkptrsize で示される範囲)がゼロ初期化されていました。しかし、エスケープ解析の結果、一部のポインタ変数が関数スコープ外にエスケープしないことが判明した場合、それらの変数はGCがスキャンする際に問題とならないため、必ずしもゼロ初期化する必要はありません。

この変更では、stkptrsize に加えて stkzerosize という新しい変数を導入します。

  • stkptrsize は引き続き、スタックフレーム内でポインタを含む可能性のある領域のサイズを表します。これはGCがスタックをスキャンする際に参照する情報です。
  • stkzerosize は、実際にゼロ初期化が必要な領域のサイズを表します。これは stkptrsize 以下になります。

コンパイラは、allocauto 関数(スタック上のローカル変数を割り当てる処理)において、各ローカル変数に対して needzero フラグを設定します。このフラグは、その変数がゼロ初期化を必要とするかどうかを示します。現在の実装では、needzero は一時的にすべての PAUTO (自動変数) に対して 1 に設定されていますが、将来的にはライブネス解析(liveness analysis)によってより正確に設定される予定です。

スタック変数のソート順序も変更され、cmpstackvar 関数において、ポインタを持つ変数に加えて needzero フラグが設定されている変数が優先的に配置されるようになりました。これにより、ゼロ初期化が必要な変数がスタックフレームの先頭に近い位置に集められ、ゼロ初期化の範囲を効率的に管理できるようになります。

ggen.c (各アーキテクチャ固有のコード生成部分) の defframe 関数では、スタックフレームのゼロ初期化を行うアセンブリコードが生成されます。この関数内で、ゼロ初期化の対象となるサイズが stkptrsize から stkzerosize に変更されました。これにより、実際にゼロ初期化が必要な領域のみがゼロで埋められるようになり、不要なメモリ操作が削減されます。

この最適化は、特に多くのローカル変数や大きなスタックフレームを持つ関数において、関数呼び出しのオーバーヘッドを削減し、全体的なパフォーマンスを向上させる効果があります。

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

このコミットでは、主に以下のファイルが変更されています。

  • src/cmd/5g/ggen.c (ARMアーキテクチャ向けコード生成)
  • src/cmd/6g/ggen.c (x86-64アーキテクチャ向けコード生成)
  • src/cmd/8g/ggen.c (x86-32アーキテクチャ向けコード生成)
  • src/cmd/gc/go.h (コンパイラの共通ヘッダファイル)
  • src/cmd/gc/pgen.c (プログラム生成の共通部分)

src/cmd/gc/go.h の変更:

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -270,6 +270,7 @@ struct	Node
 	uchar	dupok;	// duplicate definitions ok (for func)
 	schar	likely; // likeliness of if statement
 	uchar	hasbreak;	// has break statement
+	uchar	needzero; // if it contains pointers, needs to be zeroed on function entry
 	uint	esc;		// EscXXX
 	int	funcdepth;
 
@@ -940,7 +941,8 @@ EXTERN	NodeList*	lastconst;
 EXTERN	Node*	lasttype;
 EXTERN	vlong	maxarg;
 EXTERN	vlong	stksize;		// stack size for current frame
-EXTERN	vlong	stkptrsize;		// prefix of stack containing pointers for current frame
+EXTERN	vlong	stkptrsize;		// prefix of stack containing pointers
+EXTERN	vlong	stkzerosize;		// prefix of stack that must be zeroed on entry
 EXTERN	int32	blockgen;		// max block number
 EXTERN	int32	block;			// current block number
 EXTERN	int	hasdefer;		// flag that curfn has defer statetment

Node 構造体に needzero フィールドが追加され、グローバル変数として stkzerosize が追加されています。

src/cmd/gc/pgen.c の変更:

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -394,6 +394,12 @@ cmpstackvar(Node *a, Node *b)\n 	bp = haspointers(b->type);\n 	if(ap != bp)\n 		return bp - ap;\n+\n+\tap = a->needzero;\n+\tbp = b->needzero;\n+\tif(ap != bp)\n+\t\treturn bp - ap;\n+\n 	if(a->type->width < b->type->width)\n 		return +1;\
 	if(a->type->width > b->type->width)\n 		return -1;\
@@ -421,6 +428,11 @@ allocauto(Prog* ptxt)\n 	\t\tll->n->used = 0;\
 \n 	markautoused(ptxt);\
+\t\n+\t// TODO: Remove when liveness analysis sets needzero instead.\
+\tfor(ll=curfn->dcl; ll != nil; ll=ll->next)\
+\t\tif (ll->n->class == PAUTO)\
+\t\t\tll->n->needzero = 1; // ll->n->addrtaken;\
 \n \tlistsort(&curfn->dcl, cmpstackvar);\
 \n@@ -459,8 +467,11 @@ allocauto(Prog* ptxt)\
 \t\t\tfatal(\"bad width\");\
 \t\tstksize += w;\
 \t\tstksize = rnd(stksize, n->type->align);\
-\t\tif(haspointers(n->type))\
+\t\tif(haspointers(n->type)) {\
 \t\t\tstkptrsize = stksize;\
+\t\t\tif(n->needzero)\
+\t\t\t\tstkzerosize = stksize;\
+\t\t}\
 \t\tif(thechar == \'5\')\
 \t\t\tstksize = rnd(stksize, widthptr);\
 \t\tif(stksize >= (1ULL<<31)) {\
@@ -471,6 +482,7 @@ allocauto(Prog* ptxt)\
 \t}\
 \tstksize = rnd(stksize, widthptr);\
 \tstkptrsize = rnd(stkptrsize, widthptr);\
+\tstkzerosize = rnd(stkzerosize, widthptr);\
 \n \tfixautoused(ptxt);\
 \n```
`cmpstackvar` で `needzero` によるソートが追加され、`allocauto` で `needzero` の設定と `stkzerosize` の計算ロジックが追加されています。

**`src/cmd/{5,6,8}g/ggen.c` の変更:**

これらのファイルでは、`defframe` 関数内でスタックフレームのゼロ初期化を行うアセンブリコード生成ロジックが変更されています。具体的には、ゼロ初期化の対象サイズを決定する際に `stkptrsize` の代わりに `stkzerosize` が使用されるようになりました。

例 (`src/cmd/6g/ggen.c`):
```diff
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -30,16 +30,16 @@ defframe(Prog *ptxt, Bvec *bv)\
 	// so that garbage collector only sees initialized values
 	// when it looks for pointers.
 	p = ptxt;
-	if(stkptrsize >= 8*widthptr) {
+	if(stkzerosize >= 8*widthptr) {
 		p = appendp(p, AMOVQ, D_CONST, 0, D_AX, 0);
-		p = appendp(p, AMOVQ, D_CONST, stkptrsize/widthptr, D_CX, 0);
-		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkptrsize, D_DI, 0);
+		p = appendp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);
+		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);
 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);
 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);
 	} else {
-		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)
+		for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)
 			if(bvget(bv, j) || bvget(bv, j+1))
-				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\
+				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\
 	}\
 }\

stkptrsizestkzerosize に置き換えられているのが確認できます。

コアとなるコードの解説

src/cmd/gc/go.h

  • uchar needzero;: Node 構造体に追加された新しいフィールドです。これは、そのノード(変数など)がスタックフレーム上でゼロ初期化を必要とするかどうかを示すフラグです。uchar は符号なし文字型で、通常1バイトを占めます。
  • EXTERN vlong stkzerosize;: グローバル変数として追加されました。これは、現在の関数(curfn)のスタックフレームにおいて、ゼロ初期化が必要な領域のサイズ(バイト単位)を示します。

src/cmd/gc/pgen.c

cmpstackvar 関数

この関数は、スタック上のローカル変数をソートするための比較関数です。スタックフレームのレイアウトを最適化するために使用されます。 変更点:

	ap = a->needzero;
	bp = b->needzero;
	if(ap != bp)
		return bp - ap;

haspointers (ポインタを持つかどうか) による比較に加えて、needzero フラグによる比較が追加されました。これにより、needzero1 の変数(ゼロ初期化が必要な変数)が、needzero0 の変数よりもスタックフレームの先頭に近い位置に配置されるようになります。これは、ゼロ初期化の範囲を効率的に管理するために重要です。

allocauto 関数

この関数は、関数のローカル変数(自動変数)のスタックオフセットを割り当て、スタックフレームのサイズを計算する主要な関数です。 変更点:

  1. stkzerosize の初期化:
    	stksize = 0;
    	stkptrsize = 0;
    	stkzerosize = 0;
    
    関数開始時に stkzerosize もゼロに初期化されます。
  2. needzero フラグの一時的な設定:
    	// TODO: Remove when liveness analysis sets needzero instead.
    	for(ll=curfn->dcl; ll != nil; ll=ll->next)
    		if (ll->n->class == PAUTO)
    			ll->n->needzero = 1; // ll->n->addrtaken;
    
    この TODO コメントが示すように、この時点ではすべての自動変数(PAUTO)に対して needzero フラグが 1 に設定されています。これは一時的な措置であり、将来的にはより洗練されたライブネス解析(変数が「生きている」期間を分析する)によって、本当にゼロ初期化が必要な変数のみにこのフラグが設定される予定です。
  3. stkzerosize の計算:
    		if(haspointers(n->type)) {
    			stkptrsize = stksize;
    			if(n->needzero)
    				stkzerosize = stksize;
    		}
    
    スタック変数を処理するループ内で、もし変数がポインタを持ち(haspointers(n->type))、かつ needzero フラグが設定されている場合、その時点での stksizestkzerosize に設定されます。これにより、stkzerosize は、スタックフレームの先頭から、ゼロ初期化が必要な最後のポインタ変数までのサイズを示すようになります。
  4. stkzerosize のアラインメント:
    	stksize = rnd(stksize, widthptr);
    	stkptrsize = rnd(stkptrsize, widthptr);
    	stkzerosize = rnd(stkzerosize, widthptr);
    
    stkzerosizewidthptr (ポインタのサイズ、通常4バイトまたは8バイト) の倍数にアラインされます。

src/cmd/{5,6,8}g/ggen.c (例: src/cmd/6g/ggen.c)

defframe 関数

この関数は、関数のプロローグ(関数が呼び出された直後に実行されるコード)の一部として、スタックフレームの初期化(特にゼロ初期化)を行うアセンブリ命令を生成します。 変更点:

-	if(stkptrsize >= 8*widthptr) {
+	if(stkzerosize >= 8*widthptr) {
 		p = appendp(p, AMOVQ, D_CONST, 0, D_AX, 0);
-		p = appendp(p, AMOVQ, D_CONST, stkptrsize/widthptr, D_CX, 0);
-		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkptrsize, D_DI, 0);
+		p = appendp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);
+		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);
 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);
 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);
 	} else {
-		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)
+		for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)
 			if(bvget(bv, j) || bvget(bv, j+1))
-				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\
+				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\
 	}

このコードは、スタックフレームのゼロ初期化を効率的に行うためのアセンブリ命令を生成しています。

  • stkptrsizestkzerosize に置き換えられています。これは、ゼロ初期化の対象となるメモリ領域のサイズが、ポインタを含む全領域から、実際にゼロ初期化が必要な領域に限定されたことを意味します。
  • 大きなスタックフレームの場合(stkzerosize >= 8*widthptr)、REP STOSQ (x86-64の場合) のような命令を使って、指定されたバイト数だけメモリを効率的にゼロで埋めます。この命令は、stkzerosize の値に基づいてゼロ初期化を行うバイト数を決定します。
  • 小さなスタックフレームの場合、ループを使って個々のポインタワードをゼロで埋めます。ここでも、ループの範囲が stkptrsize から stkzerosize に変更されています。j=(stkptrsize-stkzerosize)/widthptr*2 の部分は、stkzerosize の範囲外にあるポインタ変数をスキップし、ゼロ初期化が必要な領域からループを開始するためのオフセット計算です。

この変更により、Goコンパイラは、スタックフレームのゼロ初期化をより賢く、より効率的に行うことができるようになります。これは、Goプログラムのランタイムパフォーマンス、特にガベージコレクションのオーバーヘッド削減に貢献します。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのコンパイラとランタイムの内部構造に関する資料
  • エスケープ解析に関するGoのドキュメント

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/gc ディレクトリ)
  • Goの公式ブログ (ガベージコレクションやコンパイラの最適化に関する記事)
  • GoのIssueトラッカーやChange List (CL) (コミットの背景や議論の詳細)
  • Goのコンパイラ設計に関する論文や発表資料
  • Goのスタック管理に関する技術記事I have generated the detailed technical explanation in Markdown format, following all your instructions and including all the required sections. The output is provided below.

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

このコミットは、Goコンパイラ(cmd/gc)において、スタックフレームのゼロ初期化の挙動を最適化するものです。具体的には、スタックフレーム内のポインタを含む領域(stkptrsize)と、実際にゼロ初期化が必要な領域(stkzerosize)を分離することで、ガベージコレクション(GC)の効率を向上させ、不要なゼロ初期化を削減します。これにより、関数呼び出し時のオーバーヘッドが軽減されます。

コミット

commit 3b4d792606bf6de962bed73e79c261d7ddd7266c
Author: Russ Cox <rsc@golang.org>
Date:   Fri Aug 16 21:45:59 2013 -0400

    cmd/gc: separate "has pointers" from "needs zeroing" in stack frame
    
    When the new call site-specific frame bitmaps are available,
    we can cut the zeroing to just those values that need it due
    to scope escaping.
    
    R=cshapiro, cshapiro
    CC=golang-dev
    https://golang.org/cl/13045043

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

https://github.com/golang/go/commit/3b4d792606bf6de962bed73e79c261d7ddd7266c

元コミット内容

cmd/gc: separate "has pointers" from "needs zeroing" in stack frame

このコミットは、Goコンパイラ(cmd/gc)において、スタックフレーム内の「ポインタを持つ領域」と「ゼロ初期化が必要な領域」を分離します。新しい呼び出しサイト固有のフレームビットマップが利用可能になった際、スコープエスケープによってゼロ初期化が必要な値のみにゼロ初期化を限定できるようになります。

変更の背景

Goのガベージコレクタは、スタック上のポインタを正確に識別し、ヒープ上のオブジェクトへの参照を追跡する必要があります。そのため、関数が呼び出される際、そのスタックフレームに割り当てられるメモリ領域は、GCが誤ったポインタを読み取ってクラッシュしたり、メモリリークを引き起こしたりしないように、適切に初期化される必要があります。特に、ポインタを含む可能性のある領域は、GCが安全にスキャンできるようにゼロ初期化されるのが一般的でした。

しかし、すべてのポインタを含む領域が常にゼロ初期化を必要とするわけではありません。例えば、ある変数がスタックに割り当てられたものの、その変数がヒープにエスケープしない(つまり、関数スコープ外から参照されない)場合、その変数の初期値がゼロでなくても、GCがその領域をスキャンする際に問題になることはありません。これまでの実装では、スタックフレーム内のポインタを含むすべての領域(stkptrsize)がゼロ初期化の対象となっていました。これは安全ではありますが、不要なゼロ初期化処理が発生し、特に大きなスタックフレームを持つ関数ではパフォーマンスのオーバーヘッドとなっていました。

このコミットの背景には、「新しい呼び出しサイト固有のフレームビットマップ」の導入があります。これは、各関数呼び出しサイトでスタックフレームのどの部分にポインタが含まれているかをより正確に追跡するためのメカニズムです。このより詳細な情報が利用可能になることで、コンパイラは「ポインタを持つ領域」と「ゼロ初期化が本当に必要な領域」を区別できるようになり、ゼロ初期化の範囲を最小限に抑えることが可能になります。これにより、GCの効率が向上し、ランタイムのパフォーマンスが改善されることが期待されます。

前提知識の解説

1. スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数に必要なローカル変数、引数、戻りアドレスなどを格納するためにメモリ上に確保される領域です。この領域はスタック上に積まれ、関数が終了すると解放されます。Goのスタックフレームは、ガベージコレクタがポインタを正確に識別できるように、ポインタ情報(スタックマップ)を持っています。

2. ポインタ (Pointer)

メモリ上の特定のアドレスを指し示す変数です。Goのガベージコレクタは、ポインタを追跡して到達可能なオブジェクトを特定し、到達不能なオブジェクトを解放します。スタックフレーム内のポインタは、ヒープ上のオブジェクトへの参照を持つことがあり、GCにとって重要な情報源となります。

3. メモリのゼロ初期化 (Memory Zeroing)

メモリ領域をすべてゼロで埋める処理です。Goでは、新しいメモリが割り当てられた際に、その内容が不定であることによる潜在的なバグやセキュリティリスクを防ぐために、ゼロ初期化が行われることがあります。特に、ポインタを含む可能性のあるメモリ領域は、GCが誤った値をポインタとして解釈しないように、ゼロ初期化されることが重要です。

4. ガベージコレクション (Garbage Collection, GC)

プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不能な)ものを自動的に解放する仕組みです。GoのGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作します。GCはスタックをスキャンしてポインタを識別し、ヒープ上のオブジェクトの到達可能性を判断します。

5. エスケープ解析 (Escape Analysis)

コンパイラが行う最適化の一つで、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。変数が関数スコープ外から参照される可能性がある場合(例えば、関数の戻り値として返される、グローバル変数に代入されるなど)、その変数は「エスケープする」と判断され、スタックではなくヒープに割り当てられます。エスケープしない変数はスタックに割り当てられ、関数終了時に自動的に解放されるため、GCの負担を軽減できます。

6. stkptrsizestkzerosize

  • stkptrsize: スタックフレームの先頭から、ポインタを含む可能性のある領域の終わりまでのサイズを示します。これまでの実装では、この領域全体がゼロ初期化の対象でした。
  • stkzerosize: このコミットで導入された新しい概念で、スタックフレームの先頭から、実際にゼロ初期化が必要な領域の終わりまでのサイズを示します。エスケープ解析の結果などに基づいて、stkptrsize よりも小さい値になる可能性があります。

7. cmd/gc (Go Compiler)

Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。スタックフレームのレイアウト、メモリ割り当て、GCのためのメタデータ生成など、ランタイムの低レベルな挙動に深く関与しています。

技術的詳細

このコミットの核心は、スタックフレームのゼロ初期化の粒度を細かくすることにあります。これまでは、スタックフレーム内でポインタを含む可能性のあるすべての領域(stkptrsize で示される範囲)がゼロ初期化されていました。しかし、エスケープ解析の結果、一部のポインタ変数が関数スコープ外にエスケープしないことが判明した場合、それらの変数はGCがスキャンする際に問題とならないため、必ずしもゼロ初期化する必要はありません。

この変更では、stkptrsize に加えて stkzerosize という新しい変数を導入します。

  • stkptrsize は引き続き、スタックフレーム内でポインタを含む可能性のある領域のサイズを表します。これはGCがスタックをスキャンする際に参照する情報です。
  • stkzerosize は、実際にゼロ初期化が必要な領域のサイズを表します。これは stkptrsize 以下になります。

コンパイラは、allocauto 関数(スタック上のローカル変数を割り当てる処理)において、各ローカル変数に対して needzero フラグを設定します。このフラグは、その変数がゼロ初期化を必要とするかどうかを示します。現在の実装では、needzero は一時的にすべての PAUTO (自動変数) に対して 1 に設定されていますが、将来的にはライブネス解析(liveness analysis)によってより正確に設定される予定です。

スタック変数のソート順序も変更され、cmpstackvar 関数において、ポインタを持つ変数に加えて needzero フラグが設定されている変数が優先的に配置されるようになりました。これにより、ゼロ初期化が必要な変数がスタックフレームの先頭に近い位置に集められ、ゼロ初期化の範囲を効率的に管理できるようになります。

ggen.c (各アーキテクチャ固有のコード生成部分) の defframe 関数では、スタックフレームのゼロ初期化を行うアセンブリコードが生成されます。この関数内で、ゼロ初期化の対象となるサイズが stkptrsize から stkzerosize に変更されました。これにより、実際にゼロ初期化が必要な領域のみがゼロで埋められるようになり、不要なメモリ操作が削減されます。

この最適化は、特に多くのローカル変数や大きなスタックフレームを持つ関数において、関数呼び出しのオーバーヘッドを削減し、全体的なパフォーマンスを向上させる効果があります。

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

このコミットでは、主に以下のファイルが変更されています。

  • src/cmd/5g/ggen.c (ARMアーキテクチャ向けコード生成)
  • src/cmd/6g/ggen.c (x86-64アーキテクチャ向けコード生成)
  • src/cmd/8g/ggen.c (x86-32アーキテクチャ向けコード生成)
  • src/cmd/gc/go.h (コンパイラの共通ヘッダファイル)
  • src/cmd/gc/pgen.c (プログラム生成の共通部分)

src/cmd/gc/go.h の変更:

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -270,6 +270,7 @@ struct	Node
 	uchar	dupok;	// duplicate definitions ok (for func)
 	schar	likely; // likeliness of if statement
 	uchar	hasbreak;	// has break statement
+	uchar	needzero; // if it contains pointers, needs to be zeroed on function entry
 	uint	esc;		// EscXXX
 	int	funcdepth;
 
@@ -940,7 +941,8 @@ EXTERN	NodeList*	lastconst;
 EXTERN	Node*	lasttype;
 EXTERN	vlong	maxarg;
 EXTERN	vlong	stksize;		// stack size for current frame
-EXTERN	vlong	stkptrsize;		// prefix of stack containing pointers for current frame
+EXTERN	vlong	stkptrsize;		// prefix of stack containing pointers
+EXTERN	vlong	stkzerosize;		// prefix of stack that must be zeroed on entry
 EXTERN	int32	blockgen;		// max block number
 EXTERN	int32	block;			// current block number
 EXTERN	int	hasdefer;		// flag that curfn has defer statetment

Node 構造体に needzero フィールドが追加され、グローバル変数として stkzerosize が追加されています。

src/cmd/gc/pgen.c の変更:

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -394,6 +394,12 @@ cmpstackvar(Node *a, Node *b)\n 	bp = haspointers(b->type);\n 	if(ap != bp)\n 		return bp - ap;\n+\n+\tap = a->needzero;\n+\tbp = b->needzero;\n+\tif(ap != bp)\n+\t\treturn bp - ap;\n+\n 	if(a->type->width < b->type->width)\n 		return +1;\
 	if(a->type->width > b->type->width)\n 		return -1;\
@@ -421,6 +428,11 @@ allocauto(Prog* ptxt)\n 	\t\tll->n->used = 0;\
 \n 	markautoused(ptxt);\
+\t\n+\t// TODO: Remove when liveness analysis sets needzero instead.\
+\tfor(ll=curfn->dcl; ll != nil; ll=ll->next)\
+\t\tif (ll->n->class == PAUTO)\
+\t\t\tll->n->needzero = 1; // ll->n->addrtaken;\
 \n \tlistsort(&curfn->dcl, cmpstackvar);\
 \n@@ -459,8 +467,11 @@ allocauto(Prog* ptxt)\
 \t\t\tfatal(\"bad width\");\
 \t\tstksize += w;\
 \t\tstksize = rnd(stksize, n->type->align);\
-\t\tif(haspointers(n->type))\
+\t\tif(haspointers(n->type)) {\
 \t\t\tstkptrsize = stksize;\
+\t\t\tif(n->needzero)\
+\t\t\t\tstkzerosize = stksize;\
+\t\t}\
 \t\tif(thechar == \'5\')\
 \t\t\tstksize = rnd(stksize, widthptr);\
 \t\tif(stksize >= (1ULL<<31)) {\
@@ -471,6 +482,7 @@ allocauto(Prog* ptxt)\
 \t}\
 \tstksize = rnd(stksize, widthptr);\
 \tstkptrsize = rnd(stkptrsize, widthptr);\
+\tstkzerosize = rnd(stkzerosize, widthptr);\
 \n \tfixautoused(ptxt);\
 \n```
`cmpstackvar` で `needzero` によるソートが追加され、`allocauto` で `needzero` の設定と `stkzerosize` の計算ロジックが追加されています。

**`src/cmd/{5,6,8}g/ggen.c` の変更:**

これらのファイルでは、`defframe` 関数内でスタックフレームのゼロ初期化を行うアセンブリコード生成ロジックが変更されています。具体的には、ゼロ初期化の対象サイズを決定する際に `stkptrsize` の代わりに `stkzerosize` が使用されるようになりました。

例 (`src/cmd/6g/ggen.c`):
```diff
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -30,16 +30,16 @@ defframe(Prog *ptxt, Bvec *bv)\
 	// so that garbage collector only sees initialized values
 	// when it looks for pointers.
 	p = ptxt;
-	if(stkptrsize >= 8*widthptr) {
+	if(stkzerosize >= 8*widthptr) {
 		p = appendp(p, AMOVQ, D_CONST, 0, D_AX, 0);
-		p = appendp(p, AMOVQ, D_CONST, stkptrsize/widthptr, D_CX, 0);
-		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkptrsize, D_DI, 0);
+		p = appendp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);
+		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);
 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);
 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);
 	} else {
-		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)
+		for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)
 			if(bvget(bv, j) || bvget(bv, j+1))
-				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\
+				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\
 	}\
 }\

stkptrsizestkzerosize に置き換えられているのが確認できます。

コアとなるコードの解説

src/cmd/gc/go.h

  • uchar needzero;: Node 構造体に追加された新しいフィールドです。これは、そのノード(変数など)がスタックフレーム上でゼロ初期化を必要とするかどうかを示すフラグです。uchar は符号なし文字型で、通常1バイトを占めます。
  • EXTERN vlong stkzerosize;: グローバル変数として追加されました。これは、現在の関数(curfn)のスタックフレームにおいて、ゼロ初期化が必要な領域のサイズ(バイト単位)を示します。

src/cmd/gc/pgen.c

cmpstackvar 関数

この関数は、スタック上のローカル変数をソートするための比較関数です。スタックフレームのレイアウトを最適化するために使用されます。 変更点:

	ap = a->needzero;
	bp = b->needzero;
	if(ap != bp)
		return bp - ap;

haspointers (ポインタを持つかどうか) による比較に加えて、needzero フラグによる比較が追加されました。これにより、needzero1 の変数(ゼロ初期化が必要な変数)が、needzero0 の変数よりもスタックフレームの先頭に近い位置に配置されるようになります。これは、ゼロ初期化の範囲を効率的に管理するために重要です。

allocauto 関数

この関数は、関数のローカル変数(自動変数)のスタックオフセットを割り当て、スタックフレームのサイズを計算する主要な関数です。 変更点:

  1. stkzerosize の初期化:
    	stksize = 0;
    	stkptrsize = 0;
    	stkzerosize = 0;
    
    関数開始時に stkzerosize もゼロに初期化されます。
  2. needzero フラグの一時的な設定:
    	// TODO: Remove when liveness analysis sets needzero instead.
    	for(ll=curfn->dcl; ll != nil; ll=ll->next)
    		if (ll->n->class == PAUTO)
    			ll->n->needzero = 1; // ll->n->addrtaken;
    
    この TODO コメントが示すように、この時点ではすべての自動変数(PAUTO)に対して needzero フラグが 1 に設定されています。これは一時的な措置であり、将来的にはより洗練されたライブネス解析(変数が「生きている」期間を分析する)によって、本当にゼロ初期化が必要な変数のみにこのフラグが設定される予定です。
  3. stkzerosize の計算:
    		if(haspointers(n->type)) {
    			stkptrsize = stksize;
    			if(n->needzero)
    				stkzerosize = stksize;
    		}
    
    スタック変数を処理するループ内で、もし変数がポインタを持ち(haspointers(n->type))、かつ needzero フラグが設定されている場合、その時点での stksizestkzerosize に設定されます。これにより、stkzerosize は、スタックフレームの先頭から、ゼロ初期化が必要な最後のポインタ変数までのサイズを示すようになります。
  4. stkzerosize のアラインメント:
    	stksize = rnd(stksize, widthptr);
    	stkptrsize = rnd(stkptrsize, widthptr);
    	stkzerosize = rnd(stkzerosize, widthptr);
    
    stkzerosizewidthptr (ポインタのサイズ、通常4バイトまたは8バイト) の倍数にアラインされます。

src/cmd/{5,6,8}g/ggen.c (例: src/cmd/6g/ggen.c)

defframe 関数

この関数は、関数のプロローグ(関数が呼び出された直後に実行されるコード)の一部として、スタックフレームの初期化(特にゼロ初期化)を行うアセンブリ命令を生成します。 変更点:

-	if(stkptrsize >= 8*widthptr) {
+	if(stkzerosize >= 8*widthptr) {
 		p = appendp(p, AMOVQ, D_CONST, 0, D_AX, 0);
-		p = appendp(p, AMOVQ, D_CONST, stkptrsize/widthptr, D_CX, 0);
-		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkptrsize, D_DI, 0);
+		p = appendp(p, AMOVQ, D_CONST, stkzerosize/widthptr, D_CX, 0);
+		p = appendp(p, ALEAQ, D_SP+D_INDIR, frame-stkzerosize, D_DI, 0);
 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);
 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);
 	} else {
-		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)
+		for(i=0, j=(stkptrsize-stkzerosize)/widthptr*2; i<stkzerosize; i+=widthptr, j+=2)
 			if(bvget(bv, j) || bvget(bv, j+1))
-				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\
+				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkzerosize+i);\
 	}

このコードは、スタックフレームのゼロ初期化を効率的に行うためのアセンブリ命令を生成しています。

  • stkptrsizestkzerosize に置き換えられています。これは、ゼロ初期化の対象となるメモリ領域のサイズが、ポインタを含む全領域から、実際にゼロ初期化が必要な領域に限定されたことを意味します。
  • 大きなスタックフレームの場合(stkzerosize >= 8*widthptr)、REP STOSQ (x86-64の場合) のような命令を使って、指定されたバイト数だけメモリを効率的にゼロで埋めます。この命令は、stkzerosize の値に基づいてゼロ初期化を行うバイト数を決定します。
  • 小さなスタックフレームの場合、ループを使って個々のポインタワードをゼロで埋めます。ここでも、ループの範囲が stkptrsize から stkzerosize に変更されています。j=(stkptrsize-stkzerosize)/widthptr*2 の部分は、stkzerosize の範囲外にあるポインタ変数をスキップし、ゼロ初期化が必要な領域からループを開始するためのオフセット計算です。

この変更により、Goコンパイラは、スタックフレームのゼロ初期化をより賢く、より効率的に行うことができるようになります。これは、Goプログラムのランタイムパフォーマンス、特にガベージコレクションのオーバーヘッド削減に貢献します。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのコンパイラとランタイムの内部構造に関する資料
  • エスケープ解析に関するGoのドキュメント

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/gc ディレクトリ)
  • Goの公式ブログ (ガベージコレクションやコンパイラの最適化に関する記事)
  • GoのIssueトラッカーやChange List (CL) (コミットの背景や議論の詳細)
  • Goのコンパイラ設計に関する論文や発表資料
  • Goのスタック管理に関する技術記事