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

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

このコミットは、Goコンパイラ(6g、AMD64アーキテクチャ向け)とランタイムにおけるメモリ操作の新しい規約を導入し、パフォーマンス最適化とコードの簡素化を図るものです。具体的には、CPUの方向フラグ(Direction Flag, DF)が常にクリアされた状態(CLD)であることを前提とするように変更され、これに伴いコンパイラが生成するmemcpymemsetに相当するコードが最適化されました。

コミット

commit 8f53bc06127bcb3f01ee2771f01277e10d2c81b2
Author: Ken Thompson <ken@golang.org>
Date:   Mon Dec 15 15:07:35 2008 -0800

    new convention, direction bit is
    always left cleared. changed
    compiler generated memcpy and
    memset to assume CLD.
    
    R=r
    OCL=21215
    CL=21215

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

https://github.com/golang/go/commit/8f53bc06127bcb3f01ee2771f01277e10d2c81b2

元コミット内容

このコミットは、Goコンパイラ(src/cmd/6g/cgen.c, src/cmd/6g/gen.c)とAMD64ランタイムの初期化コード(src/runtime/rt0_amd64.s)にわたる変更を含んでいます。主な変更点は以下の通りです。

  1. src/cmd/6g/cgen.c: memcpyに相当するメモリコピー操作のコード生成において、明示的なCLD(方向フラグクリア)命令の生成を削除。また、コピーサイズが小さい場合にREPプレフィックスを使用せず、直接MOVSQMOVSB命令を複数回発行する最適化を導入。
  2. src/cmd/6g/gen.c: memsetに相当するメモリゼロクリア操作のコード生成において、明示的なCLD命令の生成を削除。同様に、クリアサイズが小さい場合にREPプレフィックスを使用せず、直接STOSQSTOSB命令を複数回発行する最適化を導入。
  3. src/runtime/rt0_amd64.s: AMD64ランタイムの初期化ルーチン(_rt0_amd64)の冒頭にCLD命令を追加し、プログラム実行開始時に方向フラグが必ずクリアされることを保証。

変更の背景

この変更の背景には、x86/x64アーキテクチャにおけるCPUのEFLAGSレジスタ内の「方向フラグ(Direction Flag, DF)」の扱いに関する規約の確立があります。

従来のGoコンパイラは、memcpymemsetのような文字列操作命令(MOVS, STOSなど)を使用する際に、その都度CLD命令を発行して方向フラグをクリアしていました。これは、これらの命令が方向フラグの状態によってメモリのアドレスポインタ(ESI, EDIなど)をインクリメントするかデクリメントするかを決定するため、予測可能な動作を保証するためです。

しかし、プログラム全体で方向フラグが常にクリアされていることが保証されれば、個々のメモリ操作の前にCLD命令を繰り返し発行する必要がなくなります。これにより、生成されるアセンブリコードのサイズがわずかに減少し、CPUが不要な命令をフェッチ・デコード・実行するオーバーヘッドが削減され、全体的なパフォーマンス向上が期待できます。

また、このコミットでは、REPプレフィックス付きの文字列命令(REP MOVS, REP STOSなど)の特性も考慮されています。REPプレフィックスは、CXレジスタに指定された回数だけ命令を繰り返しますが、非常に小さいデータ量(例えば数バイトや数ワード)を操作する場合、REPプレフィックス自体のオーバーヘッドが、ループを展開して個々の命令を直接実行するよりも大きくなることがあります。このため、小さいサイズの場合はREPを使わない最適化が導入されました。

前提知識の解説

1. x86/x64アーキテクチャのEFLAGSレジスタと方向フラグ (DF)

  • EFLAGSレジスタ: x86/x64プロセッサには、CPUの状態や演算結果に関する様々なフラグを格納するEFLAGS(またはRFLAGS)レジスタがあります。これらのフラグは、条件分岐や特定の命令の動作に影響を与えます。
  • 方向フラグ (Direction Flag, DF): EFLAGSレジスタ内の1ビットのフラグです。DFの状態は、文字列操作命令(例: MOVS, STOS, CMPS, SCAS, LODS)の動作に影響を与えます。
    • DF = 0 (クリア状態): これらの命令は、ソースおよびデスティネーションポインタ(ESI, EDIなど)をインクリメントします。つまり、メモリを前方(低アドレスから高アドレスへ)に処理します。
    • DF = 1 (セット状態): これらの命令は、ソースおよびデスティネーションポインタをデクリメントします。つまり、メモリを後方(高アドレスから低アドレスへ)に処理します。
  • CLD命令: Direction Flag (DF) をクリア(0に設定)します。
  • STD命令: Direction Flag (DF) をセット(1に設定)します。

2. x86/x64の文字列操作命令とREPプレフィックス

  • 文字列操作命令: MOVS (Move String), STOS (Store String), CMPS (Compare String) など、メモリ上の文字列やバイト列を効率的に操作するための命令群です。これらの命令は、通常、ESI(ソースインデックス)とEDI(デスティネーションインデックス)レジスタを暗黙的に使用し、DFフラグの状態に応じてこれらのレジスタを自動的にインクリメントまたはデクリメントします。
    • MOVSQ: Quadword (8バイト) をコピー。
    • MOVSB: Byte (1バイト) をコピー。
    • STOSQ: AL/AX/EAX/RAXレジスタの内容をQuadword (8バイト) としてメモリにストア。
    • STOSB: ALレジスタの内容をByte (1バイト) としてメモリにストア。
  • REPプレフィックス: REP (Repeat) は、文字列操作命令の前に付加される命令プレフィックスです。REPが付加された文字列操作命令は、CX(またはRCX)レジスタの値が0になるまで、その命令を繰り返し実行します。各繰り返しでCXはデクリメントされます。これにより、ソフトウェアループを書くよりも高速に大量のデータを処理できます。
    • 例: REP MOVSQ は、CXレジスタに指定された回数だけMOVSQ命令を繰り返します。

3. memcpymemset

  • memcpy: C言語の標準ライブラリ関数で、指定されたメモリ領域から別のメモリ領域へバイト列をコピーします。Go言語のコンパイラは、Goコード内のメモリコピー操作を、内部的にmemcpyに相当するアセンブリ命令(通常はMOVSREPの組み合わせ)に変換します。
  • memset: C言語の標準ライブラリ関数で、指定されたメモリ領域を特定の値で埋めます。Go言語のコンパイラは、Goコード内のメモリ初期化(特にゼロクリア)操作を、内部的にmemsetに相当するアセンブリ命令(通常はSTOSREPの組み合わせ)に変換します。

4. Goコンパイラとランタイム

  • Goコンパイラ (6g): Go言語のソースコードを機械語に変換するツールです。6gは、AMD64(x86-64)アーキテクチャ向けのGoコンパイラの古い名称です。コンパイラは、Goの高級なメモリ操作を、効率的なアセンブリ命令に変換する役割を担います。
  • Goランタイム (rt0_amd64.s): Goプログラムの実行をサポートする低レベルのコード群です。これには、プログラムの起動、メモリ管理、ゴルーチン(軽量スレッド)のスケジューリングなどが含まれます。rt0_amd64.sは、AMD64システムにおけるGoプログラムのエントリポイント(プログラムが最初に実行される場所)を含むアセンブリファイルです。

技術的詳細

このコミットの技術的詳細は、以下の3つの側面から理解できます。

1. 方向フラグのグローバルな規約確立

最も重要な変更は、Goプログラムの実行において、CPUの方向フラグ(DF)が常にクリアされた状態(DF=0)であることを保証する新しい規約を確立した点です。

  • src/runtime/rt0_amd64.s の変更: プログラムが起動する際に最初に実行される_rt0_amd64関数(ランタイムのエントリポイント)の冒頭にCLD命令が追加されました。これにより、Goプログラムが制御を得る時点で、DFが確実にクリアされます。これは、オペレーティングシステムや他のライブラリがDFをどのような状態にしているかに依存せず、Goランタイムが自身の環境を初期化する責任を果たすものです。
  • コンパイラの変更 (src/cmd/6g/cgen.c, src/cmd/6g/gen.c): ランタイムによってDFがクリアされていることが保証されるため、コンパイラはmemcpymemsetに相当するコードを生成する際に、個々の文字列操作命令の前に冗長なCLD命令を発行する必要がなくなりました。これにより、生成される機械語コードから不要な命令が削除され、コードサイズが削減され、命令フェッチやデコードの効率が向上します。

2. 小さいサイズのメモリ操作の最適化

このコミットでは、memcpymemsetの対象となるデータサイズが小さい場合のパフォーマンス最適化も行われています。

  • REPプレフィックスのオーバーヘッド: REPプレフィックスは、大量のデータを効率的に処理するために設計されています。しかし、コピーまたはクリアするバイト数やワード数が非常に少ない場合(例えば、1〜3バイト/ワード)、REPプレフィックスのセットアップ(CXレジスタへのカウント設定など)と実行のオーバーヘッドが、単一の文字列命令を複数回直接実行するよりも大きくなることがあります。
  • コンパイラの変更 (src/cmd/6g/cgen.c, src/cmd/6g/gen.c):
    • cgen.c (memcpy相当): 8バイト単位のクワッドワード (q) と1バイト単位のバイト (c) の両方について、サイズが4未満の場合(q < 4またはc < 4)はREPプレフィックスを使用せず、MOVSQまたはMOVSB命令をwhileループで直接複数回発行するように変更されました。
    • gen.c (memset相当): 同様に、サイズが4未満の場合はREPプレフィックスを使用せず、STOSQまたはSTOSB命令をwhileループで直接複数回発行するように変更されました。
    • この「ループアンローリング」に似た最適化により、小さいサイズのメモリ操作における効率が向上します。

3. コードの簡素化と保守性

  • 一貫性の向上: 方向フラグの規約をグローバルに確立することで、コンパイラとランタイム間の連携が一貫したものになります。これにより、将来のコード変更や最適化が容易になります。
  • 冗長なコードの削除: 各メモリ操作の前にCLDを挿入するロジックが不要になることで、コンパイラのコード生成部分が簡素化されます。

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

src/cmd/6g/cgen.c (メモリコピー操作のコード生成)

--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -874,22 +874,29 @@ sgen(Node *n, Node *ns, int32 w)
 		// for future optimization
 		// we leave with the flag clear
 		gins(ACLD, N, N);
 	} else {
 		// normal direction
-		gins(ACLD, N, N);		// clear direction flag
-		if(q > 0) {
+		if(q >= 4) {
 			gconreg(AMOVQ, q, D_CX);
 			gins(AREP, N, N);	// repeat
 			gins(AMOVSQ, N, N);	// MOVQ *(SI)+,*(DI)+
+		} else
+		while(q > 0) {
+			gins(AMOVSQ, N, N);	// MOVQ *(SI)+,*(DI)+
+			q--;
 		}
 
-		if(c > 0) {
+		if(c >= 4) {
 			gconreg(AMOVQ, c, D_CX);
 			gins(AREP, N, N);	// repeat
 			gins(AMOVSB, N, N);	// MOVB *(SI)+,*(DI)+
+		
+		} else
+		while(c > 0) {
+			gins(AMOVSB, N, N);	// MOVB *(SI)+,*(DI)+
+			c--;
 		}
 	}
 }

src/cmd/6g/gen.c (メモリゼロクリア操作のコード生成)

--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -1037,7 +1037,7 @@ cgen_as(Node *nl, Node *nr, int op)
 {
 	Node nc, n1;
 	Type *tl;
-	uint32 w, c;
+	uint32 w, c, q;
 	int iszer;
 
 	if(nl == N)
@@ -1058,31 +1058,32 @@ cgen_as(Node *nl, Node *nr, int op)
 			if(debug['g'])
 				dump("\nclearfat", nl);
 
-			if(nl->type->width < 0)
-				fatal("clearfat %T %lld", nl->type, nl->type->width);
 			w = nl->type->width;
+			c = w % 8;	// bytes
+			q = w / 8;	// quads
 
-			if(w > 0)
-				gconreg(AMOVQ, 0, D_AX);
+			gconreg(AMOVQ, 0, D_AX);
+			nodreg(&n1, types[tptr], D_DI);
+			agen(nl, &n1);
 
-			if(w > 0) {
-				nodreg(&n1, types[tptr], D_DI);
-				agen(nl, &n1);
-				gins(ACLD, N, N);	// clear direction flag
-			}
-
-			c = w / 8;
-			if(c > 0) {
-				gconreg(AMOVQ, c, D_CX);
+			if(q >= 4) {
+				gconreg(AMOVQ, q, D_CX);
 				gins(AREP, N, N);	// repeat
 				gins(ASTOSQ, N, N);	// STOQ AL,*(DI)+
+			} else
+			while(q > 0) {
+				gins(ASTOSQ, N, N);	// STOQ AL,*(DI)+
+				q--;
 			}
 
-			c = w % 8;
-			if(c > 0) {
+			if(c >= 4) {
 				gconreg(AMOVQ, c, D_CX);
 				gins(AREP, N, N);	// repeat
 				gins(ASTOSB, N, N);	// STOB AL,*(DI)+
+			} else
+			while(c > 0) {
+				gins(ASTOSB, N, N);	// STOB AL,*(DI)+
+				c--;
 			}
 			goto ret;
 		}

src/runtime/rt0_amd64.s (AMD64ランタイムのエントリポイント)

--- a/src/runtime/rt0_amd64.s
+++ b/src/runtime/rt0_amd64.s
@@ -26,6 +26,7 @@ TEXT	_rt0_amd64(SB),7,$-8
 	MOVQ	AX, 0(R15)		// 0(R15) is stack limit (w 104b guard)
 	MOVQ	SP, 8(R15)		// 8(R15) is base
 
+	CLD				// convention is D is always left cleared
 	CALL	check(SB)
 
 	MOVL	16(SP), AX		// copy argc

コアとなるコードの解説

src/cmd/6g/cgen.csrc/cmd/6g/gen.c の変更点

これらのファイルでは、主に以下の2つの変更が行われています。

  1. CLD命令の削除:

    • 変更前は、gins(ACLD, N, N); という行が、メモリコピー(cgen.c)やメモリゼロクリア(gen.c)を行う文字列操作命令(AMOVSQ, AMOVSB, ASTOSQ, ASTOSB)の前に存在していました。これは、これらの命令が常に順方向(アドレス増加方向)に動作するように、方向フラグを明示的にクリアするためです。
    • 変更後は、この明示的なCLD命令が削除されています。これは、src/runtime/rt0_amd64.s でプログラム起動時に一度だけCLDが実行されるため、以降は方向フラグがクリアされた状態が維持されるという新しい規約に基づいています。これにより、生成されるアセンブリコードがよりコンパクトになります。
  2. 小さいサイズに対するREPプレフィックスの最適化:

    • 変更前は、q > 0(クワッドワード数)やc > 0(バイト数)であれば、常にREPプレフィックス(gins(AREP, N, N);)を使用して文字列操作命令を繰り返していました。
    • 変更後は、if(q >= 4)if(c >= 4) という条件が追加されました。これは、クワッドワード数またはバイト数が4以上の場合にのみREPプレフィックスを使用することを意味します。
    • それより小さいサイズ(q < 4 または c < 4)の場合、else while(q > 0)else while(c > 0) のブロックが実行されます。このブロックでは、REPプレフィックスを使わず、AMOVSQ/AMOVSB または ASTOSQ/ASTOSB 命令を直接、必要な回数だけ生成しています。例えば、q=1 ならAMOVSQを1回、q=2 なら2回、といった具合です。
    • この最適化は、REPプレフィックスのセットアップコストが、少数の命令を直接実行するコストよりも高くなる可能性があるためです。これにより、短いメモリブロックの操作がより効率的になります。

src/runtime/rt0_amd64.s の変更点

  • TEXT _rt0_amd64(SB),7,$-8 のセクション、具体的にはスタックポインタとスタックリミットの設定後、CALL check(SB) の直前に CLD 命令が追加されました。
  • CLD 命令の横には // convention is D is always left cleared というコメントが追加されており、この変更の意図が明確に示されています。
  • この変更により、Goプログラムが実行を開始する初期段階で、CPUの方向フラグが確実にクリアされた状態になります。これにより、Goコンパイラが生成するすべての文字列操作命令は、方向フラグの状態を気にすることなく、常に順方向に動作することを前提とできるようになります。これは、Goプログラム全体で一貫したメモリ操作の動作を保証するための基盤となります。

関連リンク

参考にした情報源リンク