[インデックス 1347] ファイルの概要
このコミットは、Goコンパイラ(6g、AMD64アーキテクチャ向け)とランタイムにおけるメモリ操作の新しい規約を導入し、パフォーマンス最適化とコードの簡素化を図るものです。具体的には、CPUの方向フラグ(Direction Flag, DF)が常にクリアされた状態(CLD)であることを前提とするように変更され、これに伴いコンパイラが生成するmemcpy
やmemset
に相当するコードが最適化されました。
コミット
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
)にわたる変更を含んでいます。主な変更点は以下の通りです。
src/cmd/6g/cgen.c
:memcpy
に相当するメモリコピー操作のコード生成において、明示的なCLD
(方向フラグクリア)命令の生成を削除。また、コピーサイズが小さい場合にREP
プレフィックスを使用せず、直接MOVSQ
やMOVSB
命令を複数回発行する最適化を導入。src/cmd/6g/gen.c
:memset
に相当するメモリゼロクリア操作のコード生成において、明示的なCLD
命令の生成を削除。同様に、クリアサイズが小さい場合にREP
プレフィックスを使用せず、直接STOSQ
やSTOSB
命令を複数回発行する最適化を導入。src/runtime/rt0_amd64.s
: AMD64ランタイムの初期化ルーチン(_rt0_amd64
)の冒頭にCLD
命令を追加し、プログラム実行開始時に方向フラグが必ずクリアされることを保証。
変更の背景
この変更の背景には、x86/x64アーキテクチャにおけるCPUのEFLAGSレジスタ内の「方向フラグ(Direction Flag, DF)」の扱いに関する規約の確立があります。
従来のGoコンパイラは、memcpy
やmemset
のような文字列操作命令(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. memcpy
とmemset
memcpy
: C言語の標準ライブラリ関数で、指定されたメモリ領域から別のメモリ領域へバイト列をコピーします。Go言語のコンパイラは、Goコード内のメモリコピー操作を、内部的にmemcpy
に相当するアセンブリ命令(通常はMOVS
とREP
の組み合わせ)に変換します。memset
: C言語の標準ライブラリ関数で、指定されたメモリ領域を特定の値で埋めます。Go言語のコンパイラは、Goコード内のメモリ初期化(特にゼロクリア)操作を、内部的にmemset
に相当するアセンブリ命令(通常はSTOS
とREP
の組み合わせ)に変換します。
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がクリアされていることが保証されるため、コンパイラはmemcpy
やmemset
に相当するコードを生成する際に、個々の文字列操作命令の前に冗長なCLD
命令を発行する必要がなくなりました。これにより、生成される機械語コードから不要な命令が削除され、コードサイズが削減され、命令フェッチやデコードの効率が向上します。
2. 小さいサイズのメモリ操作の最適化
このコミットでは、memcpy
やmemset
の対象となるデータサイズが小さい場合のパフォーマンス最適化も行われています。
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.c
と src/cmd/6g/gen.c
の変更点
これらのファイルでは、主に以下の2つの変更が行われています。
-
CLD
命令の削除:- 変更前は、
gins(ACLD, N, N);
という行が、メモリコピー(cgen.c
)やメモリゼロクリア(gen.c
)を行う文字列操作命令(AMOVSQ
,AMOVSB
,ASTOSQ
,ASTOSB
)の前に存在していました。これは、これらの命令が常に順方向(アドレス増加方向)に動作するように、方向フラグを明示的にクリアするためです。 - 変更後は、この明示的な
CLD
命令が削除されています。これは、src/runtime/rt0_amd64.s
でプログラム起動時に一度だけCLD
が実行されるため、以降は方向フラグがクリアされた状態が維持されるという新しい規約に基づいています。これにより、生成されるアセンブリコードがよりコンパクトになります。
- 変更前は、
-
小さいサイズに対する
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プログラム全体で一貫したメモリ操作の動作を保証するための基盤となります。
関連リンク
- x86 Instruction Set Reference: https://www.felixcloutier.com/x86/index.html (特に
CLD
,STD
,MOVS
,STOS
,REP
の各命令を参照) - Go Compiler Internals (古い情報を含む可能性あり): https://go.dev/doc/articles/go-compiler-internals
参考にした情報源リンク
- Intel 64 and IA-32 Architectures Software Developer's Manuals (特にVolume 1: Basic Architecture, Volume 2A/2B/2C/2D: Instruction Set Reference): https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語の初期のコミット履歴 (特にKen Thompson氏のコミット): https://github.com/golang/go/commits?author=ken%40golang.org
- Stack Overflowや技術ブログでのx86アセンブリ、特に方向フラグや
REP
プレフィックスに関する議論。