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

[インデックス 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 プレフィックスを使用して、より効率的にスタックフレームをゼロクリアしていました。stosqRAX レジスタの内容を RDI レジスタが指すメモリ位置にストアし、RDI を8バイト進めます。rep プレフィックスと組み合わせることで、RCX レジスタに指定された回数だけこの操作を繰り返すことができます。これにより、ループを明示的に記述するよりも高速にメモリをゼロクリアできます。

  • cmd/8g (x86-32): x86-32アーキテクチャ向けの clearstk は、x86-64と同様に stosl (Store String Long) 命令と rep プレフィックスを使用していました。stoslEAX レジスタの内容を 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 関数は、以下の共通のロジックを持っていました。

  1. スタックフレームサイズのチェック: plast->firstpc->to.offset <= 0 の条件で、スタックフレームのサイズが0以下であれば処理をスキップしていました。これは、スタックフレームが不要な関数(例:引数もローカル変数もない関数)の場合にゼロクリアを行わないための最適化です。
  2. コード挿入のためのコンテキスト再確立: newplist 関数で作成された命令リストの先頭にコードを挿入するために、現在の命令ポインタ (pc) を設定し、既存の命令チェーンを一時的に変更していました。
  3. スタックフレームのゼロクリア:
    • ARM (5g): MOVW 命令とループを使って、スタックポインタから指定されたオフセットまでのメモリ領域をゼロで埋めていました。これは、レジスタ (R1, R2, R3) を使ってアドレス、終了アドレス、ゼロ値を管理し、CMPBNE でループを制御する典型的なソフトウェアループでした。
    • 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 レジスタがそれぞれ対応する役割を担っていました。
  4. 元のコードへの復帰: ゼロクリア処理が完了した後、gins(ANOP, N, N)->link = p2; のように ANOP (No Operation) 命令を挿入し、そのリンクを元の命令チェーンの続き (p2) に設定することで、コンパイラが元の命令生成フローに戻れるようにしていました。

この clearstk 関数の削除は、Goコンパイラが生成するコードからこれらのゼロクリア命令が完全に排除されたことを意味します。これは、Goランタイムのガベージコレクタがスタック上のポインタを正確に追跡できるようになったため、非ポインタデータについてはゼロクリアが不要になった、あるいはコンパイラがよりスマートな方法でスタックを管理するようになった結果と考えられます。これにより、コンパイルされたバイナリのサイズが削減され、実行時のパフォーマンスが向上する可能性があります。

関連リンク

参考にした情報源リンク

  • Go言語のコンパイラに関するドキュメントやソースコード(特に src/cmd/ ディレクトリ内のコード)
  • x86およびARMアーキテクチャのアセンブリ命令セットリファレンス
  • Go言語のガベージコレクションに関する資料
  • Go言語のスタック管理に関する議論や設計ドキュメント(Goのメーリングリストやデザインドキュメントなど)
  • GoのCL (Change List) システムに関する情報