[インデックス 14031] ファイルの概要
このコミットは、Goコンパイラのcmd/5g
(ARMアーキテクチャ向けのコード生成バックエンド)における最適化を目的としています。具体的には、配列のインデックスが定数である場合に、一時レジスタの使用を避けることで、生成されるアセンブリコードの効率を向上させます。これにより、コードサイズが削減され、実行速度がわずかに向上する可能性があります。
コミット
commit ed0c5dd11f983d0e54806cd087ec852e43fa9f9e
Author: Dave Cheney <dave@cheney.net>
Date: Sat Oct 6 11:51:06 2012 +1000
cmd/5g: avoid temporary during constant OINDEX
func addr(s[]int) *int {
return &s[2]
}
--- prog list "addr" ---
0000 (/home/dfc/src/addr.go:5) TEXT addr+0(SB),$0-16
0001 (/home/dfc/src/addr.go:6) MOVW $s+0(FP),R0
0002 (/home/dfc/src/addr.go:6) MOVW 4(R0),R1
0003 (/home/dfc/src/addr.go:6) MOVW $2,R2
0004 (/home/dfc/src/addr.go:6) CMP R2,R1,
0005 (/home/dfc/src/addr.go:6) BHI ,7(APC)
0006 (/home/dfc/src/addr.go:6) BL ,runtime.panicindex+0(SB)
0007 (/home/dfc/src/addr.go:6) MOVW 0(R0),R0
0008 (/home/dfc/src/addr.go:6) MOVW $8,R1
0009 (/home/dfc/src/addr.go:6) ADD R1,R0
0010 (/home/dfc/src/addr.go:6) MOVW R0,.noname+12(FP)
0011 (/home/dfc/src/addr.go:6) RET ,
becomes
--- prog list "addr" ---
0000 (/home/dfc/src/addr.go:5) TEXT addr+0(SB),$0-16
0001 (/home/dfc/src/addr.go:6) MOVW $s+0(FP),R0
0002 (/home/dfc/src/addr.go:6) MOVW 4(R0),R1
0003 (/home/dfc/src/addr.go:6) MOVW $2,R2
0004 (/home/dfc/src/addr.go:6) CMP R2,R1,
0005 (/home/dfc/src/addr.go:6) BHI ,7(APC)
0006 (/home/dfc/src/addr.go:6) BL ,runtime.panicindex+0(SB)
0007 (/home/dfc/src/addr.go:6) MOVW 0(R0),R0
0008 (/home/dfc/src/addr.go:6) ADD $8,R0
0009 (/home/dfc/src/addr.go:6) MOVW R0,.noname+12(FP)
0010 (/home/dfc/src/addr.go:6) RET ,
R=rsc, remyoudompheng, minux.ma
CC=golang-dev
https://golang.org/cl/6590056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ed0c5dd11f983d0e54806cd087ec852e43fa9f9e
元コミット内容
cmd/5g: avoid temporary during constant OINDEX
変更の背景
Goコンパイラは、ソースコードを機械語に変換する際に様々な最適化を行います。このコミットの背景にあるのは、配列の要素に定数インデックスでアクセスする際のコード生成の非効率性です。
例えば、&s[2]
のような式では、配列 s
の先頭アドレスに、インデックス 2
と要素のサイズを掛け合わせたオフセット(2 * sizeof(int)
)を加算することで、目的の要素のアドレスを計算します。
変更前は、この定数オフセット(例: 8
バイト)をレジスタにロードし、そのレジスタの内容をベースアドレスに加算するという2段階の処理が行われていました。これは、定数値を直接加算命令のオペランドとして使用できる場合でも、不必要に一時レジスタを消費し、余分な命令を生成していました。
このコミットは、このような冗長な一時レジスタの使用を排除し、定数オフセットを直接加算命令に埋め込むことで、生成されるアセンブリコードをよりコンパクトかつ効率的にすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Goコンパイラ (
cmd/5g
):- Go言語のコンパイラは、
cmd/gc
がフロントエンドとして構文解析、型チェック、中間表現の生成などを行い、その後、各アーキテクチャ固有のバックエンド(例:cmd/5g
はARM、cmd/6g
はx86-64、cmd/8g
はx86-32)がアセンブリコードを生成します。 cmd/5g
は、ARMアーキテクチャ向けのGoプログラムをコンパイルする際に使用されるコンパイラバックエンドです。
- Go言語のコンパイラは、
-
アセンブリ言語とレジスタ:
- アセンブリ言語は、CPUが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。
- レジスタ: CPU内部にある高速な記憶領域で、計算の途中の値やアドレスを一時的に保持するために使われます。ARMアーキテクチャでは、
R0
,R1
,R2
などの汎用レジスタがあります。 - 命令:
MOVW
(Move Word),ADD
(Add),CMP
(Compare),BHI
(Branch if Higher),BL
(Branch and Link),RET
(Return) などは、ARMアセンブリの基本的な命令です。MOVW $value, Reg
: 即値value
をReg
に移動します。ADD Reg1, Reg2
:Reg2
の値にReg1
の値を加算し、結果をReg2
に格納します。ADD $value, Reg
: 即値value
をReg
に加算し、結果をReg
に格納します(このコミットで最適化される形式)。TEXT func+offset(SB),$framesize-argsize
: 関数の開始を宣言します。SB
は静的ベースレジスタ、$framesize
はスタックフレームサイズ、$argsize
は引数のサイズです。FP
(Frame Pointer): 現在のスタックフレームのベースアドレスを指すポインタ。関数引数やローカル変数へのアクセスに使われます。s+0(FP)
は、フレームポインタからのオフセットで引数s
にアクセスすることを示します。
-
Goコンパイラの内部構造(
cgen.c
):cgen.c
は、Goコンパイラのコード生成フェーズの一部であり、中間表現(ASTノードなど)をターゲットアーキテクチャのアセンブリ命令に変換する役割を担っています。Node
: コンパイラ内部でコードの各要素(変数、定数、演算など)を表すデータ構造です。nodconst(&n2, types[tptr], v*w)
: 定数ノードn2
を作成する関数です。v*w
は定数オフセットの計算結果(例:8
)です。regalloc(&n4, n2.type, N)
: レジスタn4
を割り当てる関数です。N
はノードを表します。gmove(&n2, &n4)
: ノードn2
の値をノードn4
に移動するアセンブリ命令を生成する関数です。これは通常、レジスタ間のデータ転送や、メモリからレジスタへのロード、レジスタからメモリへのストアなどに使われます。gins(optoas(OADD, types[tptr]), &n4, &n3)
: アセンブリ命令を生成する関数です。optoas(OADD, types[tptr])
: 抽象的な演算子OADD
(加算)と型types[tptr]
(ポインタ型)を、ターゲットアーキテクチャの具体的なアセンブリ命令(例:ADD
)に変換します。&n4, &n3
: 命令のオペランドとなるノードです。この場合、n4
の値をn3
に加算します。
regfree(&n4)
: レジスタn4
を解放する関数です。
-
OINDEX:
- Goコンパイラ内部で、配列やスライスのインデックスアクセスを表す抽象的な操作です。
OINDEX
は、s[2]
のような式を処理する際に使用されます。
- Goコンパイラ内部で、配列やスライスのインデックスアクセスを表す抽象的な操作です。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのコード生成器が、定数オフセットを持つアドレス計算をより効率的に行うように変更された点にあります。
変更前は、&s[2]
のような式を処理する際、コンパイラは以下のような手順でアセンブリコードを生成していました(簡略化された概念図):
- 配列のベースアドレスをレジスタ
R0
にロードします。 - 定数オフセット(例:
8
バイト)を別のレジスタR1
にロードします(MOVW $8, R1
)。 R0
とR1
の内容を加算し、結果をR0
に格納します(ADD R1, R0
)。
このプロセスでは、定数 8
を一時的に R1
レジスタに格納するという余分なステップがありました。多くのCPUアーキテクチャ(ARMを含む)では、加算命令のオペランドとして即値(定数)を直接指定できる「即値オペランド」という機能があります。これにより、レジスタを介さずに直接定数を加算できます。
変更後のコンパイラは、この即値オペランドの機能を活用するように修正されました。具体的には、src/cmd/5g/cgen.c
内の agen
関数(アドレス生成を担当する部分)で、定数インデックスによるオフセット計算が行われる際に、一時レジスタの割り当て (regalloc
)、値の移動 (gmove
)、レジスタの解放 (regfree
) のステップが削除されました。
代わりに、gins
関数が直接、定数ノード (n2
) を加算命令のオペランドとして受け取れるようになりました。これにより、コンパイラは以下のようなより効率的なアセンブリコードを生成します:
- 配列のベースアドレスをレジスタ
R0
にロードします。 R0
に定数8
を直接加算します(ADD $8, R0
)。
この変更により、MOVW $8, R1
と ADD R1, R0
の2つの命令が ADD $8, R0
の1つの命令に削減されます。これにより、生成されるバイナリのサイズがわずかに小さくなり、CPUが実行する命令数が減るため、実行速度もわずかに向上します。これは、特にループ内で頻繁にアクセスされる配列に対して効果を発揮します。
コアとなるコードの変更箇所
--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -733,11 +733,7 @@ agen(Node *n, Node *res)\n }\n \n nodconst(&n2, types[tptr], v*w);\n-\t\t\tregalloc(&n4, n2.type, N);\n-\t\t\tgmove(&n2, &n4);\n-\t\t\tgins(optoas(OADD, types[tptr]), &n4, &n3);\n-\t\t\tregfree(&n4);\n-\n+\t\t\tgins(optoas(OADD, types[tptr]), &n2, &n3);\n \t\t\tgmove(&n3, res);\
\t\t\tregfree(&n3);\
\t\t\tbreak;
コアとなるコードの解説
変更は src/cmd/5g/cgen.c
ファイルの agen
関数内で行われています。この関数は、アドレス計算(&s[2]
のような式)をアセンブリ命令に変換する役割を担っています。
-
削除された行:
regalloc(&n4, n2.type, N); gmove(&n2, &n4); gins(optoas(OADD, types[tptr]), &n4, &n3); regfree(&n4);
これらの行は、定数オフセット
n2
を一時レジスタn4
にロードし、そのn4
を使って加算命令を生成する処理を表しています。regalloc(&n4, n2.type, N);
: 一時レジスタn4
を割り当てます。gmove(&n2, &n4);
: 定数n2
の値をn4
に移動するアセンブリ命令(例:MOVW $8, R1
)を生成します。gins(optoas(OADD, types[tptr]), &n4, &n3);
:n4
の内容をn3
に加算するアセンブリ命令(例:ADD R1, R0
)を生成します。regfree(&n4);
: 一時レジスタn4
を解放します。
-
追加された行:
gins(optoas(OADD, types[tptr]), &n2, &n3);
この行は、削除された4行を置き換えるものです。ここでは、
gins
関数が直接定数ノードn2
を加算命令のオペランドとして受け取っています。これにより、コンパイラは定数オフセットを直接加算命令に埋め込むアセンブリ命令(例:ADD $8, R0
)を生成できるようになります。一時レジスタの割り当て、移動、解放のオーバーヘッドがなくなります。
この変更により、コンパイラは定数インデックスアクセスにおけるアドレス計算の際に、より最適化されたアセンブリコードを生成するようになります。
関連リンク
- Go言語のコンパイラに関するドキュメント(公式):https://go.dev/doc/compiler
- Goのソースコード(GitHub):https://github.com/golang/go
- GoのIssue Tracker(関連する最適化の議論などが見つかる可能性):https://github.com/golang/go/issues
参考にした情報源リンク
- Goのコミット履歴(GitHub):https://github.com/golang/go/commits/master
- Goのコードレビューシステム(Gerrit):https://go.dev/cl/ (コミットメッセージに記載されている
https://golang.org/cl/6590056
はGerritのリンクです) - ARMアセンブリ言語の基本(一般的な情報源)
- コンパイラ最適化の基本(一般的な情報源)
- Goコンパイラの内部構造に関するブログ記事や解説(一般的な情報源)
- Goのソースコード内のコメントや関連ファイル