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

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

このコミットは、Go言語のARMアーキテクチャ向けコンパイラであるcmd/5gにおけるコード生成の最適化に関するものです。具体的には、レジスタと定数(特にゼロ)の比較を行う際に、不要な一時レジスタの使用を排除し、生成されるアセンブリコードをより効率的にすることを目指しています。これにより、命令数の削減とレジスタの利用効率の向上が図られています。

コミット

commit 905e8dfa27b943aa306cadb1a6880bbeb4051173
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Oct 8 09:51:04 2012 +1100

    cmd/5g: avoid temporaries during gcmp(reg, constant)
    
    Address several instances of unneeded temporaries when using gcmp.
    
    func M(m map[int]bool) int {
            return len(m)
    }
    
    --- prog list "M" ---
    0000 (/home/dfc/src/map.go:3) TEXT      M+0(SB),$0-8
    0001 (/home/dfc/src/map.go:4) MOVW      m+0(FP),R0
    0002 (/home/dfc/src/map.go:4) MOVW      $0,R1
    0003 (/home/dfc/src/map.go:4) CMP       R1,R0,
    0004 (/home/dfc/src/map.go:4) BEQ       ,6(APC)
    0005 (/home/dfc/src/map.go:4) MOVW      0(R0),R0
    0006 (/home/dfc/src/map.go:4) MOVW      R0,.noname+4(FP)
    0007 (/home/dfc/src/map.go:4) RET       ,
    
    after:
    
    --- prog list "M" ---
    0000 (/home/dfc/src/map.go:3) TEXT      M+0(SB),$0-8
    0001 (/home/dfc/src/map.go:4) MOVW      m+0(FP),R0
    0002 (/home/dfc/src/map.go:4) CMP       $0,R0,
    0003 (/home/dfc/src/map.go:4) BEQ       ,5(APC)
    0004 (/home/dfc/src/map.go:4) MOVW      0(R0),R0
    0005 (/home/dfc/src/map.go:4) MOVW      R0,.noname+4(FP)
    0006 (/home/dfc/src/map.go:4) RET       ,
    
    func C(c chan int) int {
            return cap(c)
    }
    
    --- prog list "C" ---
    0000 (/home/dfc/src/map.go:3) TEXT      C+0(SB),$0-8
    0001 (/home/dfc/src/map.go:4) MOVW      c+0(FP),R0
    0002 (/home/dfc/src/map.go:4) MOVW      $0,R1
    0003 (/home/dfc/src/map.go:4) CMP       R1,R0,
    0004 (/home/dfc/src/map.go:4) BEQ       ,6(APC)
    0005 (/home/dfc/src/map.go:4) MOVW      4(R0),R0
    0006 (/home/dfc/src/map.go:4) MOVW      R0,.noname+4(FP)
    0007 (/home/dfc/src/map.go:4) RET       ,
    
    after:
    
    --- prog list "C" ---
    0000 (/home/dfc/src/map.go:3) TEXT      C+0(SB),$0-8
    0001 (/home/dfc/src/map.go:4) MOVW      c+0(FP),R0
    0002 (/home/dfc/src/map.go:4) CMP       $0,R0,
    0003 (/home/dfc/src/map.go:4) BEQ       ,5(APC)
    0004 (/home/dfc/src/map.go:4) MOVW      4(R0),R0
    0005 (/home/dfc/src/map.go:4) MOVW      R0,.noname+4(FP)
    0006 (/home/dfc/src/map.go:4) RET       ,
    
    R=rsc, minux.ma, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/6618054

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

https://github.com/golang/go/commit/905e8dfa27b943aa306cadb1a6880bbeb4051173

元コミット内容

このコミットは、Go言語のコンパイラ(特にARMアーキテクチャ向けの5g)において、レジスタと定数を比較する際のコード生成を改善するものです。以前は、定数との比較を行う際に、まず定数を一時レジスタにロードし、その一時レジスタと別のレジスタを比較するという非効率な手順を踏んでいました。このコミットは、この中間の一時レジスタの使用を排除し、レジスタと定数を直接比較するアセンブリ命令を生成するように変更します。

コミットメッセージには、len(map)cap(chan)の例が示されています。これらの関数は、内部的にマップやチャネルのポインタがnil(ゼロ)であるかどうかをチェックする際に、この非効率な比較コードが生成されていました。

変更の背景

Goコンパイラは、Go言語のソースコードを機械語に変換する役割を担っています。この変換プロセスでは、中間表現(IR)から最終的なアセンブリコードを生成する「コード生成」フェーズがあります。コード生成の目的の一つは、実行効率の高い、最適化された機械語を生成することです。

このコミットが行われた2012年当時、Goコンパイラはまだ発展途上にあり、様々な最適化の機会が存在していました。特に、特定のアーキテクチャ(この場合はARM)向けのアセンブリコード生成においては、そのアーキテクチャの命令セットの特性を最大限に活かすための細かな調整が重要でした。

レジスタと定数の比較は、プログラムの基本的な操作であり、頻繁に発生します。例えば、ポインタがnilであるかどうかのチェック(if m == nilのようなコード)は、この種の比較に該当します。もし、このような基本的な操作で不要な命令が生成されていると、プログラム全体のパフォーマンスに悪影響を及ぼします。

このコミットの背景には、Goコンパイラのコード生成における冗長性を特定し、それを排除することで、生成されるバイナリのサイズを削減し、実行速度を向上させるという目的がありました。特に、ARMのようなリソースが限られた環境では、命令数の削減はより大きな意味を持ちます。

前提知識の解説

Goコンパイラとcmd/5g

Go言語のコンパイラは、複数のコンポーネントから構成されています。cmd/5gは、Goの初期のコンパイラツールチェーンの一部であり、ARMアーキテクチャ(具体的にはARMv5/v6)向けのGoプログラムをコンパイルするために使用されていました。Goのコンパイラは、ターゲットアーキテクチャごとに異なるバックエンドを持っており、5gはそのARMバックエンドの一つでした。現在では、Goのコンパイラはより統合されたgo tool compileコマンドの下で動作し、アーキテクチャごとの命名規則(例: 5g, 6g, 8g)は使われなくなっていますが、当時のGo開発においては重要な役割を担っていました。

アセンブリ言語とレジスタ

アセンブリ言語は、CPUが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。CPUは、データを一時的に保存するために「レジスタ」と呼ばれる高速な記憶領域を持っています。プログラムの実行効率は、レジスタの効率的な利用に大きく依存します。

  • MOVW (Move Word): データをレジスタ間、またはメモリとレジスタ間で移動させる命令です。
  • CMP (Compare): 2つのオペランドを比較し、その結果に応じてCPUのフラグレジスタを設定する命令です。この命令自体は結果を保存しませんが、後続の条件分岐命令(例: BEQ)がこのフラグを利用します。
  • BEQ (Branch if Equal): 比較結果が等しい場合に、指定されたアドレスにプログラムの実行フローを分岐させる命令です。
  • TEXT: 関数の開始を示すディレクティブです。
  • RET (Return): 関数から呼び出し元に戻る命令です。
  • $0: 即値(immediate value)の0を表します。アセンブリ言語では、定数を直接命令に埋め込むことができます。
  • R0, R1: ARMアーキテクチャにおける汎用レジスタです。
  • FP (Frame Pointer): 現在のスタックフレームの基点を示すレジスタです。関数引数やローカル変数へのアクセスに使われます。
  • SB (Static Base): グローバル変数や関数への参照に使われる静的ベースレジスタです。
  • APC (Address of Program Counter): プログラムカウンタの現在値、または相対アドレス指定に使われます。

コンパイラのコード生成と最適化

コンパイラのコード生成フェーズでは、抽象的な中間表現を具体的な機械語命令に変換します。この際、様々な最適化が適用されます。

  • レジスタ割り当て (Register Allocation): 変数や中間結果をどのレジスタに割り当てるかを決定するプロセスです。レジスタは数が限られているため、効率的な割り当てが重要です。
  • 命令選択 (Instruction Selection): 中間表現の操作を、ターゲットアーキテクチャの最適なアセンブリ命令にマッピングするプロセスです。
  • 命令スケジューリング (Instruction Scheduling): 命令の実行順序を並べ替えることで、パイプラインのストールを減らし、CPUの利用率を最大化する最適化です。
  • 冗長なコードの削除 (Redundant Code Elimination): 不要な命令や計算を特定し、削除する最適化です。今回のコミットはこれに該当します。

lencap関数

Go言語の組み込み関数であるlenは、マップ、スライス、配列、文字列、チャネルの長さを返します。capは、スライスとチャネルの容量を返します。マップやチャネルの場合、これらの関数は、対象がnilであるかどうかを内部的にチェックし、nilであれば0を返します。このnilチェックの際に、ポインタがゼロであるかどうかの比較が行われます。

技術的詳細

このコミットの核心は、Goコンパイラのcmd/5gにおけるcgen.cファイル内のgcmp(generate compare)関数の呼び出し方、およびその周辺のレジスタ管理の変更です。

変更前のコードでは、レジスタと定数(特にゼロ)を比較する際に、以下のような手順を踏んでいました。

  1. nodconst(&n2, types[tptr], 0);: 定数0を表すノードn2を作成します。
  2. regalloc(&n3, n2.type, N);: 一時レジスタn3を割り当てます。
  3. gmove(&n2, &n3);: 定数ノードn2の値を一時レジスタn3に移動させます。
  4. gcmp(optoas(OCMP, types[tptr]), &n1, &n3);: レジスタn1と一時レジスタn3を比較します。
  5. regfree(&n3);: 一時レジスタn3を解放します。

このシーケンスは、定数を直接比較命令のオペランドとして使用できるARMアーキテクチャの命令セットの特性を十分に活用していませんでした。多くのRISCアーキテクチャでは、レジスタと即値(定数)を直接比較する命令(例: CMP R0, #0)が存在します。

変更後のコードでは、一時レジスタn3の割り当て、値の移動、解放のステップが削除され、gcmp関数が直接レジスタn1と定数ノードn2(値は0)を受け取るように変更されました。

gcmp(optoas(OCMP, types[tptr]), &n1, &n2);

これは、gcmp関数内部、またはその下流のコード生成ロジックが、n2が定数であることを認識し、適切な「レジスタと即値の比較」アセンブリ命令を生成するように改善されたことを意味します。

アセンブリコードの比較

コミットメッセージに示されているlen(map)cap(chan)の例は、この最適化の効果を明確に示しています。

func M(m map[int]bool) int { return len(m) } の例:

変更前のアセンブリ:

0001 MOVW      m+0(FP),R0  // マップポインタをR0にロード
0002 MOVW      $0,R1       // 定数0を一時レジスタR1にロード
0003 CMP       R1,R0,      // R1 (0) とR0 (マップポインタ) を比較
0004 BEQ       ,6(APC)     // 等しければ分岐

ここで、MOV $0,R1CMP R1,R0の2命令が、定数0を一時レジスタR1にロードし、そのレジスタを使って比較を行っています。

変更後のアセンブリ:

0001 MOVW      m+0(FP),R0  // マップポインタをR0にロード
0002 CMP       $0,R0,      // 定数0とR0 (マップポインタ) を直接比較
0003 BEQ       ,5(APC)     // 等しければ分岐

変更後では、MOV $0,R1命令が削除され、CMP $0,R0という1命令で直接レジスタR0と即値0の比較が行われています。これにより、1命令の削減と、一時レジスタR1の不要化が実現されています。

同様の最適化がcap(chan)の例でも確認できます。

この最適化は、コンパイラのバックエンドにおける命令選択とレジスタ割り当ての改善の一例であり、生成されるコードの品質向上に寄与します。

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

変更はsrc/cmd/5g/cgen.cファイルに集中しています。

--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -328,10 +328,7 @@ cgen(Node *n, Node *res)
 		cgen(nl, &n1);

 		nodconst(&n2, types[tptr], 0);
-		regalloc(&n3, n2.type, N);
-		gmove(&n2, &n3);
-		gcmp(optoas(OCMP, types[tptr]), &n1, &n3);
-		regfree(&n3);
+		gcmp(optoas(OCMP, types[tptr]), &n1, &n2);
 		p1 = gbranch(optoas(OEQ, types[tptr]), T, -1);

 		n2 = n1;
@@ -370,10 +367,7 @@ cgen(Node *n, Node *res)
 		cgen(nl, &n1);

 		nodconst(&n2, types[tptr], 0);
-		regalloc(&n3, n2.type, N);
-		gmove(&n2, &n3);
-		gcmp(optoas(OCMP, types[tptr]), &n1, &n3);
-		regfree(&n3);
+		gcmp(optoas(OCMP, types[tptr]), &n1, &n2);
 		p1 = gbranch(optoas(OEQ, types[tptr]), T, -1);

 		n2 = n1;

コアとなるコードの解説

cgen.cは、GoコンパイラのARMバックエンドにおけるコード生成の中核を担うファイルです。cgen関数は、Goの抽象構文木(AST)のノードを受け取り、それに対応するアセンブリコードを生成します。

上記の変更箇所は、cgen関数内で特定の比較操作(おそらくポインタのnilチェックなど)を処理する部分です。

  • nodconst(&n2, types[tptr], 0);: これは、型types[tptr](ポインタ型)で値が0である定数ノードn2を作成する関数呼び出しです。
  • regalloc(&n3, n2.type, N);: n2と同じ型の新しい一時レジスタn3を割り当てます。Nは、レジスタ割り当てのヒントやフラグである可能性があります。
  • gmove(&n2, &n3);: n2の値をn3に移動させるアセンブリ命令を生成します。
  • gcmp(optoas(OCMP, types[tptr]), &n1, &n3);: n1(レジスタに格納された値)とn3(一時レジスタに格納された0)を比較するアセンブリ命令を生成します。optoas(OCMP, types[tptr])は、比較操作の種類(OCMPは比較、types[tptr]は比較対象の型)を指定するものです。
  • regfree(&n3);: 一時レジスタn3を解放します。

このコミットでは、上記のregallocgmoveregfreeの3行が削除され、gcmpの呼び出しがgcmp(optoas(OCMP, types[tptr]), &n1, &n2);に変更されています。

この変更は、gcmp関数が、その第3引数としてレジスタノードだけでなく、定数ノード(n2)も直接受け取れるようになったことを示唆しています。gcmp関数内部、またはその下流の命令選択ロジックが、定数ノードを検出した場合に、レジスタと即値を直接比較するアセンブリ命令(例: ARMのCMP R0, #0)を生成するように賢くなった、と解釈できます。

これにより、不要なレジスタの割り当てと、そのレジスタへの値の移動という2つの命令が削減され、生成されるアセンブリコードがよりコンパクトで効率的になります。これは、コンパイラのバックエンドにおける典型的な最適化手法の一つです。

関連リンク

参考にした情報源リンク

  • Go言語のコンパイラに関するドキュメントやブログ記事 (一般的なGoコンパイラの構造や最適化に関する情報)
  • ARMアセンブリ言語の命令セットリファレンス (MOVW, CMP, BEQなどの命令の詳細)
  • コンパイラ最適化に関する一般的な書籍やオンラインリソース (レジスタ割り当て、命令選択、冗長コード削除などの概念)
  • Goのlencap関数の内部実装に関する情報 (特にマップやチャネルのnilチェックの挙動)
  • Goの初期のコンパイラツールチェーンに関する歴史的情報 (cmd/5gなどの役割)
  • Goのソースコード内のsrc/cmd/5g/cgen.cファイルとその関連ファイル (具体的な実装の詳細)
  • Dave Cheney氏のブログやGoコミュニティでの議論 (Goのパフォーマンス最適化に関する知見)