[インデックス 13242] ファイルの概要
このコミットは、Go言語のコンパイラ (cmd/5g
, cmd/6g
, cmd/8g
) における clearstk
関数の削除に関するものです。clearstk
関数は、関数呼び出しの際にスタックフレームをゼロクリアする役割を担っていました。この変更により、コンパイラのコードベースから冗長な処理が削除され、より効率的なスタック管理が実現されたと考えられます。
コミット
commit 96b0594833f183ef41b393af3ddced8457f9e6ef
Author: Russ Cox <rsc@golang.org>
Date: Fri Jun 1 10:10:59 2012 -0400
cmd/5g, cmd/6g, cmd/8g: delete clearstk
Dreg from https://golang.org/cl/4629042
R=ken2
CC=golang-dev
https://golang.org/cl/6259057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/96b0594833f183ef41b393af3ddced8457f9e6ef
元コミット内容
cmd/5g, cmd/6g, cmd/8g: delete clearstk
Dreg from https://golang.org/cl/4629042
R=ken2
CC=golang-dev
https://golang.org/cl/6259057
変更の背景
このコミットの背景には、Goコンパイラのスタック管理戦略の進化があります。clearstk
関数は、関数が呼び出される際にそのスタックフレームをゼロで埋める役割を担っていました。これは、セキュリティ上の理由(以前の関数のデータが残ることを防ぐ)や、デバッグの容易さ(初期化されていないメモリによる未定義動作の回避)のために行われることがあります。
しかし、Go言語のランタイムやコンパイラの最適化が進むにつれて、この明示的なスタックのゼロクリアが不要になる、あるいはより効率的な方法で代替されるようになったと考えられます。コミットメッセージにある Dreg from https://golang.org/cl/4629042
は、この変更が以前の変更セット(CL: Change List)から派生したものであることを示唆しています。元のCL 4629042は、おそらくスタック管理のより広範な変更の一部であり、clearstk
の削除はその変更の結果として行われたものと推測されます。
具体的には、Goのガベージコレクタがスタック上のポインタを正確に追跡できるようになり、非ポインタデータについてはゼロクリアの必要性が薄れた、あるいはコンパイラが生成するコードがスタック上の未使用領域を適切に管理するようになった、といった理由が考えられます。これにより、不要な命令の実行を削減し、コンパイルされたコードのパフォーマンスを向上させることが目的とされています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
- Goコンパイラ: Go言語のソースコードを機械語に変換するプログラムです。Goコンパイラは、
cmd/5g
(ARM),cmd/6g
(x86-64),cmd/8g
(x86-32) のように、ターゲットアーキテクチャごとに異なる実装を持っていました(現在は統合されています)。これらのコンパイラは、Go言語の初期のバージョンで使われていたもので、Plan 9 Cコンパイラをベースにしていました。 - スタックフレーム: 関数が呼び出されるたびに、その関数が使用するローカル変数、引数、戻りアドレスなどを格納するためにメモリ上に確保される領域です。この領域は「スタック」と呼ばれるメモリ領域に割り当てられ、LIFO (Last-In, First-Out) の原則で管理されます。
- ゼロクリア: メモリ領域の内容をすべてゼロで埋める操作です。スタックフレームのゼロクリアは、セキュリティ上の理由(以前の関数の機密データが残ることを防ぐ)や、未初期化のメモリ使用によるバグを防ぐために行われることがあります。
gsubr.c
: Goコンパイラのバックエンドにおける共通のサブルーチンやユーティリティ関数が定義されているファイルです。各アーキテクチャ (5g
,6g
,8g
) ごとに存在し、アセンブリコードの生成やレジスタ割り当てなど、アーキテクチャ固有の処理を抽象化する役割を担っていました。Prog
構造体: Goコンパイラの内部表現で、アセンブリ命令を表す構造体です。gins
関数などを使ってProg
オブジェクトを生成し、命令列を構築します。Node
構造体: Goコンパイラの内部表現で、抽象構文木 (AST) のノードや、オペランド(レジスタ、メモリ、定数など)を表す構造体です。mal
関数: メモリを割り当てるための関数です。clearp
関数:Prog
構造体をクリア(ゼロ初期化)する関数です。gins
関数: アセンブリ命令を生成し、命令リストに追加する関数です。gmove
関数: データの移動命令を生成する関数です。gbranch
関数: 分岐命令を生成する関数です。patch
関数: 分岐命令のターゲットアドレスを修正する関数です。AMOVW
,AMOVQ
,AMOVL
: それぞれ32ビット、64ビット、32ビットのデータ移動命令(Move Word, Move Quadword, Move Long)を表すアセンブリ命令のオペコードです。ACMP
: 比較命令(Compare)のオペコードです。ABNE
: 不等分岐命令(Branch Not Equal)のオペコードです。ACLD
: 方向フラグをクリアする命令(Clear Direction Flag)のオペコードです。文字列操作命令の方向を設定します。AREP
: 繰り返しプレフィックス(Repeat Prefix)のオペコードです。次の命令をCX
レジスタの値の回数だけ繰り返します。ASTOSQ
,ASTOSL
: それぞれ64ビット、32ビットのストア文字列命令(Store String Quadword, Store String Long)のオペコードです。DI
レジスタが指すメモリ位置にAX
レジスタの内容をストアし、DI
をインクリメント/デクリメントします。
技術的詳細
このコミットは、Goコンパイラのバックエンドにおけるスタックフレームのゼロクリア処理を削除しています。具体的には、src/cmd/5g/gsubr.c
, src/cmd/6g/gsubr.c
, src/cmd/8g/gsubr.c
の各ファイルから clearstk
関数が完全に削除されています。
clearstk
関数は、関数プロローグ(関数の開始部分)に挿入され、新しく確保されたスタックフレームの領域をゼロで埋めるためのアセンブリ命令を生成していました。
-
cmd/5g
(ARM): ARMアーキテクチャ向けのclearstk
は、MOVW
命令とループを使ってスタックフレームをゼロクリアしていました。具体的には、スタックポインタ (SP
) からオフセットされたアドレスから開始し、関数に必要なスタックサイズ分だけ、4バイトずつゼロを書き込んでいました。これは、R1
レジスタをポインタとして使用し、R3
レジスタにゼロを保持し、R2
レジスタを終了アドレスとして使用するループ構造でした。 -
cmd/6g
(x86-64): x86-64アーキテクチャ向けのclearstk
は、stosq
(Store String Quadword) 命令とrep
プレフィックスを使用して、より効率的にスタックフレームをゼロクリアしていました。stosq
はRAX
レジスタの内容をRDI
レジスタが指すメモリ位置にストアし、RDI
を8バイト進めます。rep
プレフィックスと組み合わせることで、RCX
レジスタに指定された回数だけこの操作を繰り返すことができます。これにより、ループを明示的に記述するよりも高速にメモリをゼロクリアできます。 -
cmd/8g
(x86-32): x86-32アーキテクチャ向けのclearstk
は、x86-64と同様にstosl
(Store String Long) 命令とrep
プレフィックスを使用していました。stosl
はEAX
レジスタの内容をEDI
レジスタが指すメモリ位置にストアし、EDI
を4バイト進めます。
これらの clearstk
関数の削除は、Goランタイムのスタック管理が進化し、これらの明示的なゼロクリアが不要になったことを示しています。Goのガベージコレクタは、スタック上のポインタを正確に識別し、非ポインタデータについてはゼロクリアの必要がないか、あるいは他のメカニズムで対処されるようになったため、コンパイラがこの処理を生成する必要がなくなったと考えられます。これにより、コンパイルされたバイナリのサイズがわずかに減少し、実行時のオーバーヘッドも削減されます。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルから clearstk
関数が完全に削除されています。
src/cmd/5g/gsubr.c
: 58行削除src/cmd/6g/gsubr.c
: 38行削除src/cmd/8g/gsubr.c
: 38行削除
具体的な削除箇所は以下の通りです。
src/cmd/5g/gsubr.c
--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -174,64 +174,6 @@ newplist(void)
return pl;
}
-void
-clearstk(void)
-{
- Plist *pl;
- Prog *p, *p1, *p2, *p3;
- Node dst, end, zero, con;
-
- if(plast->firstpc->to.offset <= 0)
- return;
-
- // reestablish context for inserting code
- // at beginning of function.
- pl = plast;
- p1 = pl->firstpc;
- p2 = p1->link;
- pc = mal(sizeof(*pc));
- clearp(pc);
- p1->link = pc;
-
- // zero stack frame
-
- // MOVW $4(SP), R1
- nodreg(&dst, types[tptr], 1);
- p = gins(AMOVW, N, &dst);
- p->from.type = D_CONST;
- p->from.reg = REGSP;
- p->from.offset = 4;
-
- // MOVW $n(R1), R2
- nodreg(&end, types[tptr], 2);
- p = gins(AMOVW, N, &end);
- p->from.type = D_CONST;
- p->from.reg = 1;
- p->from.offset = p1->to.offset;
-
- // MOVW $0, R3
- nodreg(&zero, types[TUINT32], 3);
- nodconst(&con, types[TUINT32], 0);
- gmove(&con, &zero);
-
- // L:
- // MOVW.P R3, 0(R1) +4
- // CMP R1, R2
- // BNE L
- p = gins(AMOVW, &zero, &dst);
- p->to.type = D_OREG;
- p->to.offset = 4;
- p->scond |= C_PBIT;
- p3 = p;
- p = gins(ACMP, &dst, N);
- raddr(&end, p);
- patch(gbranch(ABNE, T, 0), p3);
-
- // continue with original code.
- gins(ANOP, N, N)->link = p2;
- pc = P;
-}
-
void
gused(Node *n)
{
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -172,44 +172,6 @@ newplist(void)
return pl;
}
-void
-clearstk(void)
-{
- Plist *pl;
- Prog *p1, *p2;
- Node sp, di, cx, con, ax;
-
- if((uint32)plast->firstpc->to.offset <= 0)
- return;
-
- // reestablish context for inserting code
- // at beginning of function.
- pl = plast;
- p1 = pl->firstpc;
- p2 = p1->link;
- pc = mal(sizeof(*pc));
- clearp(pc);
- p1->link = pc;
-
- // zero stack frame
- nodreg(&sp, types[tptr], D_SP);
- nodreg(&di, types[tptr], D_DI);
- nodreg(&cx, types[TUINT64], D_CX);
- nodconst(&con, types[TUINT64], (uint32)p1->to.offset / widthptr);
- gins(ACLD, N, N);
- gins(AMOVQ, &sp, &di);
- gins(AMOVQ, &con, &cx);
- nodconst(&con, types[TUINT64], 0);
- nodreg(&ax, types[TUINT64], D_AX);
- gins(AMOVQ, &con, &ax);
- gins(AREP, N, N);
- gins(ASTOSQ, N, N);
-
- // continue with original code.
- gins(ANOP, N, N)->link = p2;
- pc = P;
-}
-
void
gused(Node *n)
{
src/cmd/8g/gsubr.c
--- a/src/cmd/8g/gsubr.c
+++ b/src/cmd/8g/gsubr.c
@@ -173,44 +173,6 @@ newplist(void)
return pl;
}
-void
-clearstk(void)
-{
- Plist *pl;
- Prog *p1, *p2;
- Node sp, di, cx, con, ax;
-
- if(plast->firstpc->to.offset <= 0)
- return;
-
- // reestablish context for inserting code
- // at beginning of function.
- pl = plast;
- p1 = pl->firstpc;
- p2 = p1->link;
- pc = mal(sizeof(*pc));
- clearp(pc);
- p1->link = pc;
-
- // zero stack frame
- nodreg(&sp, types[tptr], D_SP);
- nodreg(&di, types[tptr], D_DI);
- nodreg(&cx, types[TUINT32], D_CX);
- nodconst(&con, types[TUINT32], p1->to.offset / widthptr);
- gins(ACLD, N, N);
- gins(AMOVL, &sp, &di);
- gins(AMOVL, &con, &cx);
- nodconst(&con, types[TUINT32], 0);
- nodreg(&ax, types[TUINT32], D_AX);
- gins(AMOVL, &con, &ax);
- gins(AREP, N, N);
- gins(ASTOSL, N, N);
-
- // continue with original code.
- gins(ANOP, N, N)->link = p2;
- pc = P;
-}
-
void
gused(Node *n)
{
コアとなるコードの解説
削除された clearstk
関数は、Goコンパイラが生成するアセンブリコードの一部として、関数のプロローグ(開始部分)にスタックフレームをゼロクリアする命令を挿入していました。
各アーキテクチャの clearstk
関数は、以下の共通のロジックを持っていました。
- スタックフレームサイズのチェック:
plast->firstpc->to.offset <= 0
の条件で、スタックフレームのサイズが0以下であれば処理をスキップしていました。これは、スタックフレームが不要な関数(例:引数もローカル変数もない関数)の場合にゼロクリアを行わないための最適化です。 - コード挿入のためのコンテキスト再確立:
newplist
関数で作成された命令リストの先頭にコードを挿入するために、現在の命令ポインタ (pc
) を設定し、既存の命令チェーンを一時的に変更していました。 - スタックフレームのゼロクリア:
- ARM (
5g
):MOVW
命令とループを使って、スタックポインタから指定されたオフセットまでのメモリ領域をゼロで埋めていました。これは、レジスタ (R1
,R2
,R3
) を使ってアドレス、終了アドレス、ゼロ値を管理し、CMP
とBNE
でループを制御する典型的なソフトウェアループでした。 - x86-64 (
6g
):ACLD
(Clear Direction Flag),AMOVQ
(Move Quadword),AREP
(Repeat Prefix),ASTOSQ
(Store String Quadword) 命令を組み合わせて、効率的に64ビット単位でスタックをゼロクリアしていました。RDI
レジスタにスタックの開始アドレス、RCX
レジスタにゼロクリアするクワッドワードの数、RAX
レジスタにゼロ値を設定し、REP STOSQ
命令で一括してゼロクリアを実行していました。 - x86-32 (
8g
): x86-64と同様に、ACLD
,AMOVL
(Move Long),AREP
,ASTOSL
(Store String Long) 命令を組み合わせて、32ビット単位でスタックをゼロクリアしていました。EDI
レジスタ、ECX
レジスタ、EAX
レジスタがそれぞれ対応する役割を担っていました。
- ARM (
- 元のコードへの復帰: ゼロクリア処理が完了した後、
gins(ANOP, N, N)->link = p2;
のようにANOP
(No Operation) 命令を挿入し、そのリンクを元の命令チェーンの続き (p2
) に設定することで、コンパイラが元の命令生成フローに戻れるようにしていました。
この clearstk
関数の削除は、Goコンパイラが生成するコードからこれらのゼロクリア命令が完全に排除されたことを意味します。これは、Goランタイムのガベージコレクタがスタック上のポインタを正確に追跡できるようになったため、非ポインタデータについてはゼロクリアが不要になった、あるいはコンパイラがよりスマートな方法でスタックを管理するようになった結果と考えられます。これにより、コンパイルされたバイナリのサイズが削減され、実行時のパフォーマンスが向上する可能性があります。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットのGitHubページ: https://github.com/golang/go/commit/96b0594833f183ef41b393af3ddced8457f9e6ef
- 関連するGo Change List (CL): https://golang.org/cl/6259057
- このコミットが派生した元のGo Change List (CL): https://golang.org/cl/4629042
参考にした情報源リンク
- Go言語のコンパイラに関するドキュメントやソースコード(特に
src/cmd/
ディレクトリ内のコード) - x86およびARMアーキテクチャのアセンブリ命令セットリファレンス
- Go言語のガベージコレクションに関する資料
- Go言語のスタック管理に関する議論や設計ドキュメント(Goのメーリングリストやデザインドキュメントなど)
- GoのCL (Change List) システムに関する情報