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

[インデックス 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言語の初期段階におけるコンパイラ(6gamd64 アーキテクチャ向けのGoコンパイラ)の最適化およびデバッグに関連しています。特に、ガベージコレクション(GC)の参照カウント方式を実験的に導入する、あるいはその挙動を詳細に分析するための準備として行われたと考えられます。

コミットメッセージにある「rewrite heap pointer moves as xchg」は、メモリ上のヒープ領域にあるポインタの値をある場所から別の場所へ移動させる際に、通常のデータ転送命令(例: MOV)ではなく、xchg (exchange) 命令を使用することを意味します。xchg 命令は、2つのオペランドの値をアトミックに交換する命令です。

この変更の主な背景は以下の点が考えられます。

  1. 参照カウントGCの実験: Go言語は最終的にトレース型GC(マーク&スイープなど)を採用しましたが、初期には参照カウントGCも検討されていた可能性があります。参照カウントGCでは、オブジェクトへの参照が追加または削除されるたびに、そのオブジェクトの参照カウントを更新する必要があります。ポインタの移動は参照カウントの変更を伴うため、この操作を特殊な命令(xchg)で置き換えることで、参照カウントの更新ロジックをフックしたり、その挙動をデバッグしたりしやすくする目的があったと考えられます。
  2. デバッグとプロファイリング: xchg 命令を使用することで、特定のメモリ操作を識別しやすくなります。コンパイラのデバッグフラグ (-r または debug['r']) と組み合わせることで、ヒープポインタの移動が発生した際に、通常のデータ移動とは異なる命令シーケンスを生成させ、その挙動を追跡したり、パフォーマンスを分析したりすることが可能になります。コミット内のコメントにも「for now, mark ref count updates with AXCHGQ.」とあり、参照カウントの更新を AXCHGQ でマークする意図が明確に示されています。
  3. アトミック性: xchg 命令は、メモリとレジスタ間で値をアトミックに交換する特性を持ちます。これはマルチスレッド環境でのデータ競合を防ぐ上で重要ですが、このコミットの文脈では、主にデバッグや特定のセマンティクスを付与する目的が強いと考えられます。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が必要です。

  1. Goコンパイラ 6g:
    • Go言語の初期のコンパイラ群は、ターゲットアーキテクチャに応じて命名されていました。6gamd64 (64-bit Intel/AMD) アーキテクチャ向けのGoコンパイラです。同様に 8g386 (32-bit Intel/AMD)、5gARM 向けでした。これらはPlan 9 Cコンパイラをベースにしていました。
    • これらのコンパイラは、Goのソースコードをアセンブリコードに変換する役割を担っていました。
  2. ヒープとポインタ:
    • ヒープ: プログラムが実行時に動的にメモリを確保する領域です。Goでは makenew で確保されたオブジェクトはヒープに配置されます。
    • ポインタ: メモリ上の特定のアドレスを指し示す変数です。Goでは、参照型(スライス、マップ、チャネル、インターフェース、関数、ポインタ)の値はヒープに格納され、それらへのアクセスはポインタを介して行われます。
  3. ガベージコレクション (GC):
    • プログラムが動的に確保したメモリのうち、もはや使用されなくなった領域を自動的に解放する仕組みです。これにより、メモリリークを防ぎ、開発者が手動でメモリ管理を行う負担を軽減します。
    • 参照カウントGC: 各オブジェクトが、それを参照しているポインタの数を記録します。参照カウントが0になったオブジェクトは、もはや到達不可能と判断され、メモリが解放されます。循環参照の問題や、参照カウントの更新オーバーヘッドが課題となることがあります。
    • トレース型GC: ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトを追跡し、到達不可能なオブジェクトを「ガベージ」として回収します。Goの現在のGCは、並行マーク&スイープ方式です。
  4. xchg 命令 (Exchange):
    • x86/x64アーキテクチャのCPU命令の一つで、2つのオペランド(レジスタとレジスタ、またはレジスタとメモリ)の値をアトミックに交換します。
    • 例: XCHG AX, BXAX レジスタと BX レジスタの値を交換します。XCHG EAX, [EBX]EAX レジスタと EBX が指すメモリ位置の値を交換します。
    • アトミック性: xchg 命令は、その実行中に他のCPUやコアからのアクセスをブロックし、操作が中断されないことを保証します。これは、マルチプロセッサシステムで共有データへのアクセスを同期する際に特に重要です。
  5. コンパイラのコード生成と最適化:
    • コンパイラは、高水準言語のコードを低水準の機械語(アセンブリコード)に変換します。
    • コード生成: 抽象構文木 (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 ラベル内の処理は以下の通りです。

  1. debug['r'] フラグが設定されていない場合(つまり、Russ Cox氏のデバッグモードがオフの場合)は、通常の st ラベル(MOV 命令を生成する場所)にフォールバックします。これは、この最適化/デバッグ機能がデフォルトでは無効であることを意味します。
  2. debug['r'] が有効な場合、コメントに「for now, mark ref count updates with AXCHGQ.」とあるように、参照カウントの更新を AXCHGQ (64-bit) または AXCHGL (32-bit) 命令でマークします。
  3. この 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.cregopt 関数も変更されています。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 の変更

  • ポインタ型の分離: 以前は TPTR32TPTR64TINT32/TUINT32 および TINT64/TUINT64 と同じ MOV 命令 (AMOVL/AMOVQ) を生成するケースにまとめられていました。この変更により、ポインタ型が独立した case として扱われるようになりました。
  • refcount ロジックの導入:
    • TPTR32 または TPTR64gmove 呼び出しにおいて、移動先 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コンパイラのバックエンドにおける低レベルなコード生成と最適化の挙動を調整し、特にガベージコレクションに関連するポインタ操作のデバッグと分析を可能にするためのものです。

関連リンク

参考にした情報源リンク

  • コミット情報: /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"