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

[インデックス 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比較のコード生成をより直接的かつ効率的にすることにありました。これにより、コンパイラが生成するアセンブリコードが簡潔になり、実行時のパフォーマンスが向上することが期待されました。

前提知識の解説

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

  1. Goコンパイラ (cmd/6g, cmd/8g):

    • cmd/6gは、Go言語のソースコードをAMD64(64ビット)アーキテクチャ向けのアセンブリコードにコンパイルするコンパイラです。
    • cmd/8gは、Go言語のソースコードをx86(32ビット)アーキテクチャ向けのアセンブリコードにコンパイルするコンパイラです。
    • これらはGoの初期のコンパイラであり、Goのツールチェーンの一部として機能していました。現在のGoコンパイラはより統合されたgo tool compileコマンドの下で動作しますが、基本的なコード生成のロジックは共通しています。
  2. アセンブリ言語と命令:

    • 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): 汎用レジスタの一つで、多くの場合、演算結果の一時的な格納や関数からの戻り値の格納に使用されます。
  3. nilの表現:

    • Go言語において、nilはポインタ、スライス、マップ、チャネル、インターフェース、関数などのゼロ値を表します。内部的には、これらの型のnil値は通常、メモリ上のアドレスが0(またはそれに相当する値)として表現されます。したがって、nilとの比較は、多くの場合、値が0であるかどうかの比較に帰着します。
  4. コンパイラのコード生成フェーズ:

    • コンパイラは、ソースコードを機械語に変換する過程で、複数のフェーズを経ます。このコミットが関連するのは、中間表現からアセンブリコードを生成する「コード生成」フェーズです。
    • agen (Address Generation): アドレスを計算し、そのアドレスをレジスタに格納する処理を指します。
    • igen (Indirect Generation): 値を間接的に(アドレスを介して)ロードする処理を指します。
  5. Goのデータ構造の内部表現:

    • スライス (Slice): Goのスライスは、内部的には3つの要素を持つ構造体として表現されます: データへのポインタ、長さ、容量。nilスライスは、データへのポインタがnil(0)である状態です。
    • インターフェース (Interface): Goのインターフェースは、内部的には2つの要素を持つ構造体として表現されます: 型情報へのポインタと、値へのポインタ。nilインターフェースは、両方のポインタがnil(0)である状態です。
    • Array_arrayは、スライスの内部構造におけるデータポインタのオフセットを示す定数であると推測されます。

技術的詳細

このコミットの核心は、Goコンパイラがnil比較を行う際のアセンブリコード生成ロジックの変更にあります。

変更前は、スライスやインターフェースのnil比較において、コンパイラは以下のような手順を踏んでいました。

  1. 比較対象の変数(例: err)のアドレスを計算し、一時的なレジスタ(例: AX)にロードする(LEAQ err+n(SP), AX)。
  2. そのレジスタが指すメモリ位置から値(スライスやインターフェースのデータポインタ)を読み込み、別のレジスタに格納する(MOVQ (AX), AX)。
  3. そのレジスタの値と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.csrc/cmd/8g/cgen.cbgen関数内で行われています。この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;
    • n1n2にコピーし、n2の操作タイプをOINDREG(間接レジスタ)に設定します。これは、n2がレジスタに格納されたアドレスが指すメモリの内容を参照することを示します。
  • n2.xoffset = Array_array; (スライスの場合) または n2.xoffset = 0; (インターフェースの場合)
    • スライスの場合、Array_arrayはスライス構造体内のデータポインタへのオフセットです。インターフェースの場合、オフセット0はインターフェース構造体内の値ポインタを指します。これにより、間接参照の対象がスライス/インターフェースのデータ/値ポインタになります。
  • gins(optoas(OCMP, types[tptr]), &n2, &tmp);
    • ginsはアセンブリ命令を生成します。ここでは、OCMP(比較命令)を生成し、n2(間接参照された値)とtmpnilを表す0定数)を比較します。

この一連の処理は、まず変数のアドレスをレジスタにロードし、次にそのアドレスを介して間接的に値を取得してから比較するという、二段階の操作を行っていました。

変更後:

  • igen(nl, &n1, N);
    • igenは「間接生成」を意味します。これは、nlが指すメモリ位置から直接値をロードし、それをn1に格納します。これにより、agenOINDREGを使った間接参照のステップが不要になります。n1は、直接ロードされた値を保持するレジスタを表します。
  • n1.xoffset += Array_array; (スライスの場合) または n1.type = types[tptr]; (インターフェースの場合)
    • スライスの場合、igenがロードした値はスライス構造体全体のアドレスであるため、Array_arrayオフセットを加算してデータポインタのアドレスに調整します。インターフェースの場合、igenは既に値ポインタをロードしているため、型情報を設定するだけで済みます。
  • gins(optoas(OCMP, types[tptr]), &n1, &tmp);
    • ginsOCMP命令を生成し、n1(直接ロードされた値)とtmpnilを表す0定数)を比較します。

この変更により、コンパイラはnil比較において、対象の値を直接レジスタにロードし、0と比較するようになりました。これにより、LEAQ命令とそれに続く間接参照が不要になり、生成されるアセンブリコードがより効率的で簡潔になります。これは、Goのコンパイラがより洗練され、生成されるコードの品質が向上したことを示しています。

関連リンク

参考にした情報源リンク

  • Go言語のコンパイラに関するドキュメントやソースコード(Goの公式リポジトリ)
  • x86-64アセンブリ言語の命令セットに関する資料(MOVQ, LEAQ, CMPQなど)
  • Go言語のスライスとインターフェースの内部表現に関する資料
  • コンパイラの最適化に関する一般的な情報
  • https://github.com/golang/go/commit/ff642e290f8e8ced8be26324838febda2ae3c534 (本コミットのGitHubページ)
  • https://golang.org/cl/6493099 (本コミットのGerrit Code Reviewページ)