[インデックス 13796] ファイルの概要
このコミットは、Goコンパイラ(cmd/6g
およびcmd/8g
)におけるnil
比較時のコード生成を最適化するものです。具体的には、不要なagen
(アドレス生成)命令を排除し、より効率的なアセンブリコードを生成することで、特にif err := foo(); err != nil {...}
のような一般的なイディオムにおいて、パフォーマンスの向上と生成されるコードの簡潔化を図っています。
コミット
commit ff642e290f8e8ced8be26324838febda2ae3c534
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Tue Sep 11 08:08:40 2012 +0200
cmd/6g, cmd/8g: eliminate extra agen for nil comparisons.
Removes an extra LEAL/LEAQ instructions there and usually saves
a useless temporary in the idiom
if err := foo(); err != nil {...}
Generated code is also less involved:
MOVQ err+n(SP), AX
CMPQ AX, $0
(potentially CMPQ n(SP), $0) instead of
LEAQ err+n(SP), AX
CMPQ (AX), $0
Update #1914.
R=daniel.morsing, nigeltao, rsc
CC=golang-dev, remy
https://golang.org/cl/6493099
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ff642e290f8e8ced8be26324838febda2ae3c534
元コミット内容
cmd/6g, cmd/8g: eliminate extra agen for nil comparisons.
このコミットは、nil
との比較を行う際に、コンパイラが生成するアセンブリコードから余分なagen
(アドレス生成)命令を削除します。これにより、LEAL
/LEAQ
命令が不要になり、通常はif err := foo(); err != nil {...}
のようなイディオムで発生する無駄な一時変数の使用を削減します。
生成されるコードはより簡潔になります。具体的には、
MOVQ err+n(SP), AX
CMPQ AX, $0
(場合によってはCMPQ n(SP), $0
)
のようになる代わりに、
LEAQ err+n(SP), AX
CMPQ (AX), $0
のようなコードが生成されていました。
これはIssue #1914の更新に関連しています。
変更の背景
Go言語では、エラーハンドリングにおいてif err := foo(); err != nil {...}
というパターンが非常に頻繁に用いられます。このパターンは、関数呼び出しの結果として返されるエラーがnil
でない場合に特定のエラー処理を行うためのものです。
従来のGoコンパイラ(cmd/6g
はAMD64アーキテクチャ向け、cmd/8g
はx86アーキテクチャ向け)では、このようなnil
との比較を行う際に、最適化が不十分な部分がありました。具体的には、比較対象の変数のアドレスをレジスタにロードし(LEAL
/LEAQ
命令)、そのレジスタが指すメモリの内容を間接的に参照してnil
(通常は0)と比較するという、回りくどいコードが生成されていました。
この余分なアドレス生成と間接参照は、CPUサイクルを無駄にし、生成されるバイナリのサイズをわずかに増加させる原因となっていました。特に、nil
比較がコードベース全体で非常に頻繁に行われることを考えると、この非効率性は累積的にパフォーマンスに影響を与える可能性がありました。
このコミットの目的は、この非効率性を解消し、nil
比較のコード生成をより直接的かつ効率的にすることにありました。これにより、コンパイラが生成するアセンブリコードが簡潔になり、実行時のパフォーマンスが向上することが期待されました。
前提知識の解説
このコミットを理解するためには、以下の概念についての基本的な知識が必要です。
-
Goコンパイラ (
cmd/6g
,cmd/8g
):cmd/6g
は、Go言語のソースコードをAMD64(64ビット)アーキテクチャ向けのアセンブリコードにコンパイルするコンパイラです。cmd/8g
は、Go言語のソースコードをx86(32ビット)アーキテクチャ向けのアセンブリコードにコンパイルするコンパイラです。- これらはGoの初期のコンパイラであり、Goのツールチェーンの一部として機能していました。現在のGoコンパイラはより統合された
go tool compile
コマンドの下で動作しますが、基本的なコード生成のロジックは共通しています。
-
アセンブリ言語と命令:
MOVQ
(Move Quadword): 64ビットのデータを移動する命令です。例えば、メモリからレジスタへ、またはレジスタからメモリへデータをコピーします。LEAQ
(Load Effective Address Quadword): 64ビットの有効アドレスをレジスタにロードする命令です。これは、メモリの内容を読み込むのではなく、メモリのアドレス計算結果をレジスタに格納するために使用されます。例えば、LEAQ offset(%base_reg), %target_reg
は、%base_reg + offset
のアドレスを%target_reg
に格納します。CMPQ
(Compare Quadword): 64ビットの値を比較する命令です。通常、2つのオペランドを比較し、その結果に基づいてCPUのフラグレジスタを設定します。これらのフラグは、その後の条件分岐命令(例:JE
(Jump if Equal),JNE
(Jump if Not Equal)など)で使用されます。SP
(Stack Pointer): スタックの現在のトップを指すレジスタです。関数内のローカル変数や引数は、スタック上に配置されることが多く、n(SP)
のような表記はスタックポインタからのオフセットを示します。AX
(Accumulator Register): 汎用レジスタの一つで、多くの場合、演算結果の一時的な格納や関数からの戻り値の格納に使用されます。
-
nil
の表現:- Go言語において、
nil
はポインタ、スライス、マップ、チャネル、インターフェース、関数などのゼロ値を表します。内部的には、これらの型のnil
値は通常、メモリ上のアドレスが0
(またはそれに相当する値)として表現されます。したがって、nil
との比較は、多くの場合、値が0
であるかどうかの比較に帰着します。
- Go言語において、
-
コンパイラのコード生成フェーズ:
- コンパイラは、ソースコードを機械語に変換する過程で、複数のフェーズを経ます。このコミットが関連するのは、中間表現からアセンブリコードを生成する「コード生成」フェーズです。
agen
(Address Generation): アドレスを計算し、そのアドレスをレジスタに格納する処理を指します。igen
(Indirect Generation): 値を間接的に(アドレスを介して)ロードする処理を指します。
-
Goのデータ構造の内部表現:
- スライス (Slice): Goのスライスは、内部的には3つの要素を持つ構造体として表現されます: データへのポインタ、長さ、容量。
nil
スライスは、データへのポインタがnil
(0)である状態です。 - インターフェース (Interface): Goのインターフェースは、内部的には2つの要素を持つ構造体として表現されます: 型情報へのポインタと、値へのポインタ。
nil
インターフェースは、両方のポインタがnil
(0)である状態です。 Array_array
は、スライスの内部構造におけるデータポインタのオフセットを示す定数であると推測されます。
- スライス (Slice): Goのスライスは、内部的には3つの要素を持つ構造体として表現されます: データへのポインタ、長さ、容量。
技術的詳細
このコミットの核心は、Goコンパイラがnil
比較を行う際のアセンブリコード生成ロジックの変更にあります。
変更前は、スライスやインターフェースのnil
比較において、コンパイラは以下のような手順を踏んでいました。
- 比較対象の変数(例:
err
)のアドレスを計算し、一時的なレジスタ(例:AX
)にロードする(LEAQ err+n(SP), AX
)。 - そのレジスタが指すメモリ位置から値(スライスやインターフェースのデータポインタ)を読み込み、別のレジスタに格納する(
MOVQ (AX), AX
)。 - そのレジスタの値と
0
を比較する(CMPQ AX, $0
)。
このプロセスでは、LEAQ
命令によってアドレスをレジスタにロードし、その後CMPQ (AX), $0
のように間接参照を行うという、余分なステップが含まれていました。特に、nil
比較は単に値が0
であるかどうかをチェックするだけでよいため、アドレスをレジスタにロードするLEAQ
命令は不要でした。
変更後は、コンパイラはigen
(indirect generation)という概念を導入し、nil
比較の対象が直接メモリ上の値である場合、その値を直接レジスタにロードし、0
と比較するように変更されました。
具体的には、
- スライスの場合: スライスのデータポインタ(
Array_array
オフセットにある)を直接レジスタにロードし、0
と比較します。 - インターフェースの場合: インターフェースの値ポインタ(オフセット
0
にある)を直接レジスタにロードし、0
と比較します。
これにより、LEAQ
命令とそれに続く間接参照が不要になり、MOVQ
命令で直接メモリから値をレジスタにロードし、CMPQ
命令で0
と比較するという、より直接的なアセンブリコードが生成されるようになります。
コミットメッセージの例で示されているように、
変更前:
LEAQ err+n(SP), AX // errのアドレスをAXにロード
CMPQ (AX), $0 // AXが指すメモリの内容を0と比較
変更後:
MOVQ err+n(SP), AX // errの値をAXに直接ロード
CMPQ AX, $0 // AXの値を0と比較
または、コンパイラが最適化して直接メモリ上の値を比較できる場合は、
CMPQ n(SP), $0 // errの値を直接0と比較
となります。この変更により、命令数が減少し、CPUのパイプライン効率が向上し、結果として実行速度がわずかに向上します。また、生成されるバイナリコードのサイズも削減されます。
コードの変更点を見ると、agen(nl, &n1); n2 = n1; n2.op = OINDREG; ... gins(optoas(OCMP, types[tptr]), &n2, &tmp);
のようなパターンが、igen(nl, &n1, N); ... gins(optoas(OCMP, types[tptr]), &n1, &tmp);
に置き換えられています。これは、agen
でアドレスを生成し、OINDREG
(間接レジスタ)を使って間接参照を行う代わりに、igen
で直接値をロードするアプローチに切り替わったことを示しています。
コアとなるコードの変更箇所
変更は主にsrc/cmd/6g/cgen.c
とsrc/cmd/8g/cgen.c
のbgen
関数内で行われています。このbgen
関数は、Goのコンパイラが条件分岐(if
文など)を処理し、アセンブリコードを生成する部分です。
src/cmd/6g/cgen.c
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -978,41 +978,35 @@ bgen(Node *n, int true, int likely, Prog *to)
if(isslice(nl->type)) {
- // only valid to cmp darray to literal nil
+ // front end should only leave cmp to literal nil
if((a != OEQ && a != ONE) || nr->op != OLITERAL) {
- yyerror("illegal array comparison");
+ yyerror("illegal slice comparison");
break;
}
a = optoas(a, types[tptr]);
- regalloc(&n1, types[tptr], N);
- agen(nl, &n1);
- n2 = n1;
- n2.op = OINDREG;
- n2.xoffset = Array_array;
- n2.type = types[tptr];
+ igen(nl, &n1, N);
+ n1.xoffset += Array_array;
+ n1.type = types[tptr];
nodconst(&tmp, types[tptr], 0);
- gins(optoas(OCMP, types[tptr]), &n2, &tmp);
+ gins(optoas(OCMP, types[tptr]), &n1, &tmp);
patch(gbranch(a, types[tptr], likely), to);
regfree(&n1);
break;
}
if(isinter(nl->type)) {
- // front end shold only leave cmp to literal nil
+ // front end should only leave cmp to literal nil
if((a != OEQ && a != ONE) || nr->op != OLITERAL) {
yyerror("illegal interface comparison");
break;
}
a = optoas(a, types[tptr]);
- regalloc(&n1, types[tptr], N);
- agen(nl, &n1);
- n2 = n1;
- n2.op = OINDREG;
- n2.xoffset = 0;
+ igen(nl, &n1, N);
+ n1.type = types[tptr];
nodconst(&tmp, types[tptr], 0);
- gins(optoas(OCMP, types[tptr]), &n2, &tmp);
+ gins(optoas(OCMP, types[tptr]), &n1, &tmp);
patch(gbranch(a, types[tptr], likely), to);
regfree(&n1);
break;
src/cmd/8g/cgen.c
--- a/src/cmd/8g/cgen.c
+++ b/src/cmd/8g/cgen.c
@@ -969,18 +969,15 @@ bgen(Node *n, int true, int likely, Prog *to)
if(isslice(nl->type)) {
// front end should only leave cmp to literal nil
if((a != OEQ && a != ONE) || nr->op != OLITERAL) {
- yyerror("illegal array comparison");
+ yyerror("illegal slice comparison");
break;
}
a = optoas(a, types[tptr]);
- regalloc(&n1, types[tptr], N);
- agen(nl, &n1);
- n2 = n1;
- n2.op = OINDREG;
- n2.xoffset = Array_array;
- n2.type = types[tptr];
+ igen(nl, &n1, N);
+ n1.xoffset += Array_array;
+ n1.type = types[tptr];
nodconst(&tmp, types[tptr], 0);
- gins(optoas(OCMP, types[tptr]), &n2, &tmp);
+ gins(optoas(OCMP, types[tptr]), &n1, &tmp);
patch(gbranch(a, types[tptr], likely), to);
regfree(&n1);
break;
@@ -993,13 +990,10 @@ bgen(Node *n, int true, int likely, Prog *to)
\tbreak;
}
a = optoas(a, types[tptr]);
- regalloc(&n1, types[tptr], N);
- agen(nl, &n1);
- n2 = n1;
- n2.op = OINDREG;
- n2.xoffset = 0;
+ igen(nl, &n1, N);
+ n1.type = types[tptr];
nodconst(&tmp, types[tptr], 0);
- gins(optoas(OCMP, types[tptr]), &n2, &tmp);
+ gins(optoas(OCMP, types[tptr]), &n1, &tmp);
patch(gbranch(a, types[tptr], likely), to);
regfree(&n1);
break;
コアとなるコードの解説
このコミットの主要な変更点は、スライスとインターフェースのnil
比較を処理する部分です。
変更前:
regalloc(&n1, types[tptr], N); agen(nl, &n1);
regalloc
はレジスタを割り当てます。agen(nl, &n1)
は、ノードnl
(比較対象の変数)のアドレスを生成し、それをn1
に格納します。このn1
は、アドレスを保持するレジスタを表します。
n2 = n1; n2.op = OINDREG;
n1
をn2
にコピーし、n2
の操作タイプをOINDREG
(間接レジスタ)に設定します。これは、n2
がレジスタに格納されたアドレスが指すメモリの内容を参照することを示します。
n2.xoffset = Array_array;
(スライスの場合) またはn2.xoffset = 0;
(インターフェースの場合)- スライスの場合、
Array_array
はスライス構造体内のデータポインタへのオフセットです。インターフェースの場合、オフセット0
はインターフェース構造体内の値ポインタを指します。これにより、間接参照の対象がスライス/インターフェースのデータ/値ポインタになります。
- スライスの場合、
gins(optoas(OCMP, types[tptr]), &n2, &tmp);
gins
はアセンブリ命令を生成します。ここでは、OCMP
(比較命令)を生成し、n2
(間接参照された値)とtmp
(nil
を表す0
定数)を比較します。
この一連の処理は、まず変数のアドレスをレジスタにロードし、次にそのアドレスを介して間接的に値を取得してから比較するという、二段階の操作を行っていました。
変更後:
igen(nl, &n1, N);
igen
は「間接生成」を意味します。これは、nl
が指すメモリ位置から直接値をロードし、それをn1
に格納します。これにより、agen
とOINDREG
を使った間接参照のステップが不要になります。n1
は、直接ロードされた値を保持するレジスタを表します。
n1.xoffset += Array_array;
(スライスの場合) またはn1.type = types[tptr];
(インターフェースの場合)- スライスの場合、
igen
がロードした値はスライス構造体全体のアドレスであるため、Array_array
オフセットを加算してデータポインタのアドレスに調整します。インターフェースの場合、igen
は既に値ポインタをロードしているため、型情報を設定するだけで済みます。
- スライスの場合、
gins(optoas(OCMP, types[tptr]), &n1, &tmp);
gins
でOCMP
命令を生成し、n1
(直接ロードされた値)とtmp
(nil
を表す0
定数)を比較します。
この変更により、コンパイラはnil
比較において、対象の値を直接レジスタにロードし、0
と比較するようになりました。これにより、LEAQ
命令とそれに続く間接参照が不要になり、生成されるアセンブリコードがより効率的で簡潔になります。これは、Goのコンパイラがより洗練され、生成されるコードの品質が向上したことを示しています。
関連リンク
- Go Issue 1914: https://github.com/golang/go/issues/1914
- Gerrit Code Review 6493099: https://golang.org/cl/6493099
参考にした情報源リンク
- Go言語のコンパイラに関するドキュメントやソースコード(Goの公式リポジトリ)
- x86-64アセンブリ言語の命令セットに関する資料(
MOVQ
,LEAQ
,CMPQ
など) - Go言語のスライスとインターフェースの内部表現に関する資料
- コンパイラの最適化に関する一般的な情報
- https://github.com/golang/go/commit/ff642e290f8e8ced8be26324838febda2ae3c534 (本コミットのGitHubページ)
- https://golang.org/cl/6493099 (本コミットのGerrit Code Reviewページ)