[インデックス 1466] ファイルの概要
このコミットは、Goコンパイラ 6g
において、ヒープポインタの移動(ムーブ)操作を xchg
(exchange) 命令に書き換える最適化を導入するものです。特に、6g -r
フラグ(Russ Cox氏のデバッグ/実験用フラグ)が有効な場合に適用されます。これにより、ガベージコレクション(GC)の参照カウント更新に関連するヒープポインタの操作が、より効率的またはデバッグしやすい形で行われるようになります。
コミット
commit 8fb60768c39ce4ad77068fb507a8beab306a1fd3
Author: Russ Cox <rsc@golang.org>
Date: Tue Jan 13 13:46:09 2009 -0800
in 6g -r (the rsc flag),
rewrite heap pointer moves as xchg.
R=ken
OCL=22665
CL=22665
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8fb60768c39ce4ad77068fb507a8beab306a1fd3
元コミット内容
6g
コンパイラにおいて、-r
フラグ(Russ Cox氏のフラグ)が有効な場合、ヒープポインタの移動操作を xchg
命令に書き換える。
変更の背景
この変更は、Go言語の初期段階におけるコンパイラ(6g
は amd64
アーキテクチャ向けのGoコンパイラ)の最適化およびデバッグに関連しています。特に、ガベージコレクション(GC)の参照カウント方式を実験的に導入する、あるいはその挙動を詳細に分析するための準備として行われたと考えられます。
コミットメッセージにある「rewrite heap pointer moves as xchg」は、メモリ上のヒープ領域にあるポインタの値をある場所から別の場所へ移動させる際に、通常のデータ転送命令(例: MOV
)ではなく、xchg
(exchange) 命令を使用することを意味します。xchg
命令は、2つのオペランドの値をアトミックに交換する命令です。
この変更の主な背景は以下の点が考えられます。
- 参照カウントGCの実験: Go言語は最終的にトレース型GC(マーク&スイープなど)を採用しましたが、初期には参照カウントGCも検討されていた可能性があります。参照カウントGCでは、オブジェクトへの参照が追加または削除されるたびに、そのオブジェクトの参照カウントを更新する必要があります。ポインタの移動は参照カウントの変更を伴うため、この操作を特殊な命令(
xchg
)で置き換えることで、参照カウントの更新ロジックをフックしたり、その挙動をデバッグしたりしやすくする目的があったと考えられます。 - デバッグとプロファイリング:
xchg
命令を使用することで、特定のメモリ操作を識別しやすくなります。コンパイラのデバッグフラグ (-r
またはdebug['r']
) と組み合わせることで、ヒープポインタの移動が発生した際に、通常のデータ移動とは異なる命令シーケンスを生成させ、その挙動を追跡したり、パフォーマンスを分析したりすることが可能になります。コミット内のコメントにも「for now, mark ref count updates with AXCHGQ.」とあり、参照カウントの更新をAXCHGQ
でマークする意図が明確に示されています。 - アトミック性:
xchg
命令は、メモリとレジスタ間で値をアトミックに交換する特性を持ちます。これはマルチスレッド環境でのデータ競合を防ぐ上で重要ですが、このコミットの文脈では、主にデバッグや特定のセマンティクスを付与する目的が強いと考えられます。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が必要です。
- Goコンパイラ
6g
:- Go言語の初期のコンパイラ群は、ターゲットアーキテクチャに応じて命名されていました。
6g
はamd64
(64-bit Intel/AMD) アーキテクチャ向けのGoコンパイラです。同様に8g
は386
(32-bit Intel/AMD)、5g
はARM
向けでした。これらはPlan 9 Cコンパイラをベースにしていました。 - これらのコンパイラは、Goのソースコードをアセンブリコードに変換する役割を担っていました。
- Go言語の初期のコンパイラ群は、ターゲットアーキテクチャに応じて命名されていました。
- ヒープとポインタ:
- ヒープ: プログラムが実行時に動的にメモリを確保する領域です。Goでは
make
やnew
で確保されたオブジェクトはヒープに配置されます。 - ポインタ: メモリ上の特定のアドレスを指し示す変数です。Goでは、参照型(スライス、マップ、チャネル、インターフェース、関数、ポインタ)の値はヒープに格納され、それらへのアクセスはポインタを介して行われます。
- ヒープ: プログラムが実行時に動的にメモリを確保する領域です。Goでは
- ガベージコレクション (GC):
- プログラムが動的に確保したメモリのうち、もはや使用されなくなった領域を自動的に解放する仕組みです。これにより、メモリリークを防ぎ、開発者が手動でメモリ管理を行う負担を軽減します。
- 参照カウントGC: 各オブジェクトが、それを参照しているポインタの数を記録します。参照カウントが0になったオブジェクトは、もはや到達不可能と判断され、メモリが解放されます。循環参照の問題や、参照カウントの更新オーバーヘッドが課題となることがあります。
- トレース型GC: ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトを追跡し、到達不可能なオブジェクトを「ガベージ」として回収します。Goの現在のGCは、並行マーク&スイープ方式です。
xchg
命令 (Exchange):- x86/x64アーキテクチャのCPU命令の一つで、2つのオペランド(レジスタとレジスタ、またはレジスタとメモリ)の値をアトミックに交換します。
- 例:
XCHG AX, BX
はAX
レジスタとBX
レジスタの値を交換します。XCHG EAX, [EBX]
はEAX
レジスタとEBX
が指すメモリ位置の値を交換します。 - アトミック性:
xchg
命令は、その実行中に他のCPUやコアからのアクセスをブロックし、操作が中断されないことを保証します。これは、マルチプロセッサシステムで共有データへのアクセスを同期する際に特に重要です。
- コンパイラのコード生成と最適化:
- コンパイラは、高水準言語のコードを低水準の機械語(アセンブリコード)に変換します。
- コード生成: 抽象構文木 (AST) などの中間表現から、ターゲットCPUの命令を生成するプロセスです。
- 最適化: 生成されるコードのパフォーマンス(速度、サイズ)を向上させるための様々な変換です。レジスタ割り当て、命令スケジューリング、不要なコードの削除などが含まれます。
gmove
関数: Goコンパイラにおける、ある場所から別の場所へデータを移動させるための汎用的なコード生成関数。regopt
関数: レジスタ割り当てや命令の最適化を行う関数。
技術的詳細
このコミットの技術的詳細は、Goコンパイラ 6g
のコード生成バックエンドにおける、ポインタ型 (TPTR32
, TPTR64
) の gmove
(汎用ムーブ) 処理の変更と、それに伴うレジスタ最適化 (regopt
) の調整にあります。
変更の核心は、gmove
関数内でポインタ型 (TPTR32
または TPTR64
) の値を移動する際に、特定の条件(特にヒープポインタの移動が参照カウントの更新を伴う場合)で通常の MOV
命令 (AMOVL
, AMOVQ
) ではなく、XCHG
命令 (AXCHGL
, AXCHGQ
) を使用するように変更された点です。
具体的には、gmove
関数において、t
(移動先のノード) がポインタ型であり、かつそれがスタックポインタ (D_SP
) ではない OINDREG
(レジスタ間接参照) または ONAME
(名前付き変数) の場合、refcount
ラベルにジャンプする新しいロジックが追加されています。
refcount
ラベル内の処理は以下の通りです。
debug['r']
フラグが設定されていない場合(つまり、Russ Cox氏のデバッグモードがオフの場合)は、通常のst
ラベル(MOV
命令を生成する場所)にフォールバックします。これは、この最適化/デバッグ機能がデフォルトでは無効であることを意味します。debug['r']
が有効な場合、コメントに「for now, mark ref count updates with AXCHGQ.」とあるように、参照カウントの更新をAXCHGQ
(64-bit) またはAXCHGL
(32-bit) 命令でマークします。- この
xchg
命令を生成する前に、移動元f
の値を一時レジスタ (nod
) にムーブし、その一時レジスタと移動先t
の間でxchg
を行います。これにより、元のgmove
のセマンティクス(res = n
)を維持しつつ、xchg
命令を挿入しています。コメントには「using a temporary on the left, so no semantic changes. code is likely slower, but still correct.」とあり、セマンティクスは変わらないが、一時レジスタを使うためコードは遅くなる可能性があると述べられています。これは、この変更がパフォーマンス最適化というよりは、デバッグや挙動分析のためのものであることを示唆しています。
また、src/cmd/6g/reg.c
の regopt
関数も変更されています。regopt
はレジスタ割り当てと命令の最適化を行う部分です。
AXCHGB
, AXCHGW
, AXCHGL
, AXCHGQ
といった xchg
命令が、レジスタの使用状況を追跡する regopt
のロジックに追加されています。xchg
命令は、そのオペランドを「読み込み」と「書き込み」の両方で使用するため、r->use1.b[z] |= bit.b[z];
(使用ビットを設定) と r->set.b[z] |= bit.b[z];
(設定ビットを設定) の両方が行われるように修正されています。これにより、コンパイラのレジスタ割り当て器が xchg
命令を正しく扱い、レジスタのライブネス分析(どのレジスタがどの時点で有効か)を正確に行えるようになります。
コアとなるコードの変更箇所
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -511,15 +511,49 @@ gmove(Node *f, Node *t)
goto st;
case TINT32:
case TUINT32:
- case TPTR32:
a = AMOVL;
goto st;
case TINT64:
case TUINT64:
- case TPTR64:
a = AMOVQ;
goto st;
+ case TPTR32:
+ case TPTR64:
+ /*
+ * store to pointer.
+ */
+ if(tt == TPTR32)
+ a = AMOVL;
+ else
+ a = AMOVQ;
+ switch(t->op) {
+ default:
+ dump("gmove to", t);
+ fatal("gmove t %O", t->op);
+
+ case OINDREG:
+ if(t->val.u.reg != D_SP)
+ goto refcount;
+ break;
+
+ case ONAME:
+ switch(t->class) {
+ default:
+ dump("gmove", t);
+ fatal("gmove t %O class %d reg %R", t->op, t->class, t->val.u.reg);
+ case PEXTERN:
+ case PSTATIC:
+ goto refcount;
+ break;
+ case PAUTO:
+ case PPARAM:
+ break;
+ }
+ break;
+ }
+ goto st;
+
st:
if(f->op == OCONST) {
gins(a, f, t);
@@ -532,6 +566,22 @@ gmove(Node *f, Node *t)
regfree(&nod);
return;
+ refcount:
+ if(!debug['r'])
+ goto st;
+ // for now, mark ref count updates with AXCHGQ.
+ // using a temporary on the left, so no semantic
+ // changes. code is likely slower, but still correct.
+ if(t64)
+ a = AXCHGQ;
+ else
+ a = AXCHGL;
+ regalloc(&nod, t->type, f);
+ gmove(f, &nod);
+ gins(a, &nod, t);
+ regfree(&nod);
+ return;
+
case TFLOAT32:
a = AMOVSS;
goto fst;
src/cmd/6g/reg.c
--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -182,6 +182,19 @@ regopt(Prog *firstp)
for(z=0; z<BITS; z++)
r->use1.b[z] |= bit.b[z];
break;
+
+ /*
+ * left side read+write
+ */
+ case AXCHGB:
+ case AXCHGW:
+ case AXCHGL:
+ case AXCHGQ:
+ for(z=0; z<BITS; z++) {
+ r->use1.b[z] |= bit.b[z];
+ r->set.b[z] |= bit.b[z];
+ }
+ break;
}
bit = mkvar(r, &p->to);
@@ -313,6 +326,11 @@ regopt(Prog *firstp)
case ASBBL:
case ASBBQ:
+ case AXCHGB:
+ case AXCHGW:
+ case AXCHGL:
+ case AXCHGQ:
+
case AADDSD:
case AADDSS:
case ACMPSD:
src/cmd/6g/cgen.c
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -7,6 +7,7 @@
/*
* generate:
*\tres = n;
+ * simplifies and calls gmove.
*/
void
cgen(Node *n, Node *res)
このファイルへの変更はコメントの追加のみで、機能的な変更はありません。cgen
関数が gmove
を呼び出すことを示唆しています。
コアとなるコードの解説
src/cmd/6g/gsubr.c
の変更
- ポインタ型の分離: 以前は
TPTR32
とTPTR64
がTINT32
/TUINT32
およびTINT64
/TUINT64
と同じMOV
命令 (AMOVL
/AMOVQ
) を生成するケースにまとめられていました。この変更により、ポインタ型が独立したcase
として扱われるようになりました。 refcount
ロジックの導入:TPTR32
またはTPTR64
のgmove
呼び出しにおいて、移動先t
のオペレーション (t->op
) がOINDREG
(レジスタ間接参照) またはONAME
(名前付き変数) の場合、さらに詳細なチェックが行われます。OINDREG
の場合、移動先がスタックポインタ (D_SP
) でない限り、refcount
ラベルにジャンプします。スタック上のポインタは通常、GCの対象外であるか、参照カウントの更新が不要なケースが多いため、除外されています。ONAME
の場合、変数のクラス (t->class
) がPEXTERN
(外部変数) またはPSTATIC
(静的変数) であれば、refcount
ラベルにジャンプします。これらはヒープ上のオブジェクトへのポインタである可能性が高く、参照カウントの更新が必要になるケースです。PAUTO
(自動変数、スタック) やPPARAM
(パラメータ、スタック) は除外されます。
refcount
ラベルの処理:if(!debug['r']) goto st;
:debug['r']
フラグが設定されていない場合、通常のMOV
命令を生成するst
ラベルに処理を移します。これにより、この特殊な挙動はデバッグモードでのみ有効になります。if(t64) a = AXCHGQ; else a = AXCHGL;
: ターゲットが64ビット (t64
) ならAXCHGQ
(64ビット交換)、そうでなければAXCHGL
(32ビット交換) 命令を選択します。regalloc(&nod, t->type, f);
: 一時的なノードnod
を割り当てます。これはxchg
命令の片方のオペランドとして使用されます。gmove(f, &nod);
: 移動元f
の値を一時ノードnod
にムーブします。gins(a, &nod, t);
: 選択されたxchg
命令 (AXCHGQ
またはAXCHGL
) を生成し、一時ノードnod
と移動先t
の間で値を交換します。regfree(&nod);
: 一時ノードを解放します。
この一連の処理により、特定のヒープポインタの移動が、通常の MOV
ではなく XCHG
命令としてコンパイルされるようになります。これは、参照カウントGCの実験やデバッグにおいて、ポインタの「ムーブ」操作を特別なものとして識別し、その挙動を追跡するためのメカニズムを提供します。
src/cmd/6g/reg.c
の変更
AXCHG*
命令のレジスタ使用状況の更新:regopt
関数は、命令がレジスタをどのように使用(読み込みuse1
、書き込みset
)するかを分析し、レジスタ割り当てを最適化します。AXCHGB
,AXCHGW
,AXCHGL
,AXCHGQ
(バイト、ワード、ロング、クワッドワードのxchg
命令) がswitch
文に追加されました。- これらの命令は、オペランドの値を交換するため、両方のオペランドが「読み込まれ」て「書き込まれる」と見なされます。したがって、
r->use1.b[z] |= bit.b[z];
(使用ビットを設定) とr->set.b[z] |= bit.b[z];
(設定ビットを設定) の両方が実行されるように修正されています。これにより、レジスタ割り当て器がxchg
命令によってレジスタの内容が変更されることを正しく認識し、後続のコード生成で正しいレジスタが使用されることを保証します。 - また、
regopt
の別のswitch
文にもAXCHG*
命令が追加されており、これは命令のタイプに基づいて特定の最適化パスを適用するためのものです。
これらの変更は、Goコンパイラのバックエンドにおける低レベルなコード生成と最適化の挙動を調整し、特にガベージコレクションに関連するポインタ操作のデバッグと分析を可能にするためのものです。
関連リンク
- Go言語の初期のコンパイラに関する情報:
- Go's original compiler toolchain - The Go Programming Language (Go 1.4リリースノートのコンパイラに関する記述)
- Go's Toolchain - The Go Programming Language (現在のGoツールチェインの概要)
- x86
XCHG
命令に関する情報:- Intel® 64 and IA-32 Architectures Software Developer’s Manuals (Volume 2A: Instruction Set Reference, A-M)
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/1466.txt
- GitHubコミットページ: https://github.com/golang/go/commit/8fb60768c39ce4ad77068fb507a8beab306a1fd3
- Go言語のコンパイラとGCに関する一般的な知識
- x86アセンブリ言語の
XCHG
命令に関する一般的な知識 - (必要に応じて)Google検索による追加情報収集
- "Go 6g compiler"
- "Go garbage collection history"
- "xchg instruction atomic"
- "Go compiler gmove"
- "Go compiler regopt"