[インデックス 14000] ファイルの概要
このコミットは、Go言語のARMアーキテクチャ向けコンパイラである cmd/5g
のコードジェネレータ (src/cmd/5g/cgen.c
) における最適化に関するものです。具体的には、定数を含む二項演算のコード生成時に、一時レジスタの使用を回避することで、より効率的なアセンブリコードを生成することを目指しています。
コミット
commit 7b36acc4acbe8509b312e6a1263ced9ddd7b54fb
Author: Dave Cheney <dave@cheney.net>
Date: Tue Oct 2 08:12:39 2012 +1000
cmd/5g: avoid temporary during constant binary op
This CL is an attempt to backport the abop code generation from 6g. This improves the generation of the range offset if the increment can be encoded directly via Operand2 shift encoding.
0023 (/home/dfc/src/range.go:7) BGE ,29(APC)
0024 (/home/dfc/src/range.go:7) MOVW 0(R3),R5
0025 (/home/dfc/src/range.go:7) MOVW $4,R1
0026 (/home/dfc/src/range.go:7) ADD R1,R3,R3
0027 (/home/dfc/src/range.go:8) ADD R5,R4,R4
0028 (/home/dfc/src/range.go:7) B ,17(APC)
becomes
0023 (/home/dfc/src/range.go:7) BGE ,28(APC)
0024 (/home/dfc/src/range.go:7) MOVW 0(R3),R0
0025 (/home/dfc/src/range.go:7) ADD $4,R3,R3
0026 (/home/dfc/src/range.go:8) ADD R0,R4,R4
0027 (/home/dfc/src/range.go:7) B ,17(APC)
Benchmarks are unimpressive
dfc@qnap:~/go/test/bench/go1$ ~/go/misc/benchcmp {old,new}.txt
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 2147483647 2147483647 +0.93%
BenchmarkFannkuch11 2147483647 2147483647 -2.52%
BenchmarkGobDecode 196135200 195842000 -0.15%
BenchmarkGobEncode 78581650 76734450 -2.35%
BenchmarkGzip 2147483647 2147483647 -0.47%
BenchmarkGunzip 1087243000 1070254000 -1.56%
BenchmarkJSONEncode 1107558000 1146077000 +3.48%
BenchmarkJSONDecode 2147483647 2147483647 -0.07%
BenchmarkMandelbrot200 2147483647 2147483647 -0.77%
BenchmarkParse 74328550 71653400 -3.60%
BenchmarkRevcomp 111123900 109325950 -1.62%
BenchmarkTemplate 2147483647 2147483647 -0.82%
benchmark old MB/s new MB/s speedup
BenchmarkGobDecode 3.91 3.92 1.00x
BenchmarkGobEncode 9.77 10.00 1.02x
BenchmarkGzip 3.65 3.66 1.00x
BenchmarkGunzip 17.85 18.13 1.02x
BenchmarkJSONEncode 1.75 1.69 0.97x
BenchmarkJSONDecode 0.83 0.83 1.00x
BenchmarkParse 0.78 0.81 1.04x
BenchmarkRevcomp 22.87 23.25 1.02x
BenchmarkTemplate 0.84 0.85 1.01x
R=remyoudompheng, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6564067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7b36acc4acbe8509b312e6a1263ced9ddd7b54fb
元コミット内容
このコミットは、Goコンパイラの cmd/5g
(ARMアーキテクチャ向け) において、定数を含む二項演算のコード生成時に一時レジスタの使用を回避する変更を導入します。
具体的には、6g
(x86-64アーキテクチャ向け) から abop
(asymmetric binary operation) のコード生成ロジックをバックポートしようとする試みです。これにより、インクリメント値がARMの Operand2
シフトエンコーディングによって直接エンコードできる場合に、レンジオフセットの生成が改善されます。
コミットメッセージには、変更前後のアセンブリコードの例が示されており、MOVW
命令が削減されることで、より簡潔なコードが生成されることが分かります。
ベンチマーク結果は「unimpressive (印象的ではない)」とされていますが、いくつかのベンチマークでわずかな改善が見られます。
変更の背景
Goコンパイラは、異なるCPUアーキテクチャ向けに複数のバックエンドを持っています。5g
はARM、6g
はx86-64、8g
はx86を担当します。コンパイラの最適化は、各アーキテクチャの特性を最大限に活かすために行われます。
この変更の背景には、6g
コンパイラで既に実装されていた abop
(非対称二項演算) のコード生成最適化を 5g
(ARM) にも適用し、コードの効率性を向上させるという目的があります。特に、ループのインクリメントなど、定数による加算や減算が頻繁に行われる場面で、一時レジスタへの定数ロードを省略し、直接命令に定数を埋め込むことで、命令数を削減し、実行速度の向上やコードサイズの縮小を図ろうとしています。
ARMアーキテクチャには、特定の定数を命令の第二オペランド (Operand2
) として直接エンコードできる機能があり、この機能を活用することで、MOVW
(Move Word) 命令のようなレジスタロード命令を不要にできます。このコミットは、そのARMの特性を活かした最適化を 5g
に導入するものです。
前提知識の解説
Goコンパイラとアーキテクチャ別バックエンド
Go言語のコンパイラは、ソースコードを機械語に変換する役割を担います。Goコンパイラは、ターゲットとするCPUアーキテクチャごとに異なるバックエンドを持っています。
cmd/5g
: ARMアーキテクチャ (32-bit) 向けのGoコンパイラバックエンド。cmd/6g
: x86-64アーキテクチャ (64-bit) 向けのGoコンパイラバックエンド。cmd/8g
: x86アーキテクチャ (32-bit) 向けのGoコンパイラバックエンド。
これらのバックエンドは、Go言語の抽象構文木 (AST) を受け取り、それぞれのアーキテクチャの命令セットに合わせたアセンブリコードを生成します。
src/cmd/5g/cgen.c
cgen.c
は "code generation" の略で、Goコンパイラのバックエンドにおける主要なコード生成ロジックを含むC言語のソースファイルです。このファイルは、GoプログラムのASTをトラバースし、対応するARMアセンブリ命令を生成する役割を担っています。レジスタ割り当て、命令選択、最適化などがここで行われます。
abop
(Asymmetric Binary Operation)
abop
は "asymmetric binary operation" の略で、非対称二項演算を指します。これは、A + B
のような二項演算において、オペランド A
と B
の性質が異なる場合に適用される最適化の概念です。例えば、一方がレジスタに格納された変数で、もう一方が定数である場合などが典型的なケースです。このような場合、コンパイラは両方のオペランドを同じように扱うのではなく、定数オペランドの特性を活かした特別なコード生成パスを選択できます。
ARMアーキテクチャと Operand2
シフトエンコーディング
ARM (Advanced RISC Machine) は、主にモバイルデバイスや組み込みシステムで広く使用されているRISC (Reduced Instruction Set Computer) ベースのCPUアーキテクチャです。ARMプロセッサの命令セットは、効率的なコード実行のために設計されており、その特徴の一つに柔軟なオペランド指定方法があります。
Operand2
: 多くのARMデータ処理命令(例:ADD
,SUB
,AND
,OR
,XOR
)では、第二オペランドとしてレジスタ、即値 (immediate value)、またはレジスタとシフト演算の組み合わせを指定できます。- 即値のエンコーディング: ARM命令セットでは、32ビットの即値すべてを直接命令に埋め込むことはできません。代わりに、8ビットの即値と4ビットのローテート量(偶数ビットのみ)を組み合わせて32ビットの定数を表現する「回転即値 (rotated immediate)」という形式がよく用いられます。これにより、特定のパターンを持つ定数(例: 0x00000004, 0x00000100, 0xFF000000 など)は、追加の
MOVW
命令なしに直接命令にエンコードできます。 - シフトエンコーディング:
Operand2
は、レジスタの値をシフト(論理左シフトLSL
、論理右シフトLSR
、算術右シフトASR
、ローテート右シフトROR
)した結果を第二オペランドとして使用することも可能です。
このコミットの最適化は、特に「小さな整数定数」が Operand2
の即値エンコーディングの制約に合致する場合に、その定数を一時レジスタにロードする MOVW
命令を省略し、直接 ADD
などの命令に埋め込むことで、命令数を削減し、コードの効率を高めるものです。
一時レジスタ (Temporary Register)
コンパイラは、複雑な計算の中間結果を一時的に保持するために、CPUの汎用レジスタを使用します。これらのレジスタは「一時レジスタ」と呼ばれます。一時レジスタの使用を減らすことは、レジスタの競合を緩和し、メモリへのスピル(レジスタの内容をメモリに退避させること)を減らすことにつながり、結果としてコードの実行効率を向上させます。
技術的詳細
このコミットの技術的詳細の中心は、src/cmd/5g/cgen.c
内の abop
(非対称二項演算) のコード生成ロジックの変更です。
変更前は、二項演算 n
(例: nl + nr
) のコードを生成する際、左右のオペランド nl
と nr
のUllman数(レジスタ必要数を示すヒューリスティック値)を比較し、より多くのレジスタを必要とする側から処理を開始していました。どちらのオペランドも、regalloc
(レジスタ割り当て) と cgen
(コード生成) を通じて、最終的にレジスタにロードされていました。
変更後のコードでは、特に OADD
(加算), OSUB
(減算), OAND
(論理AND), OOR
(論理OR), OXOR
(論理XOR) といった特定の二項演算において、右オペランド (nr
) が「小さな整数定数 (smallintconst
)」である場合に、特別な処理を行うようになりました。
smallintconst(nr)
のチェック: この関数(コミットの差分には含まれていませんが、その存在が示唆されています)は、nr
がARMのOperand2
として直接エンコード可能な形式の小さな整数定数であるかを判定します。例えば、4
のような値は、追加のMOVW
命令なしにADD
命令の即値として直接埋め込むことができます。- 一時レジスタの回避:
- もし
nr
がsmallintconst
であれば、n2 = *nr;
のように、nr
のノード自体をn2
にコピーします。この場合、regalloc(&n2, nr->type, N);
とcgen(nr, &n2);
の呼び出しはスキップされます。これにより、定数をレジスタにロードするためのMOVW
命令が不要になります。 n2
はもはやレジスタを指すものではなく、直接命令に埋め込まれるリテラル値として扱われます。
- もし
regfree
の条件付き実行: 最後にregfree(&n2);
が呼び出される部分も変更されました。if(n2.op != OLITERAL) regfree(&n2);
となり、n2
がリテラル(OLITERAL
はGoコンパイラ内部でリテラルノードを表すオペレーションコード)である場合は、レジスタの解放を行わないようになりました。これは、リテラル値はレジスタを消費しないため、解放する必要がないという論理に基づいています。
コミットメッセージに示されたアセンブリコードの例は、この最適化の効果を明確に示しています。
変更前:
0025 (/home/dfc/src/range.go:7) MOVW $4,R1 ; 定数4をR1にロード
0026 (/home/dfc/src/range.go:7) ADD R1,R3,R3 ; R3にR1(定数4)を加算
ここでは、定数 4
を R1
という一時レジスタにロードするために MOVW
命令が使用され、その後 ADD
命令でそのレジスタが使用されています。
変更後:
0025 (/home/dfc/src/range.go:7) ADD $4,R3,R3 ; R3に定数4を直接加算
変更後では、$4
という定数が ADD
命令の Operand2
として直接エンコードされ、MOVW
命令が完全に削除されています。これにより、命令数が1つ削減され、コードサイズが小さくなり、実行効率が向上します。
ベンチマーク結果は「unimpressive」とされていますが、これはこの特定の最適化が全体的なパフォーマンスに劇的な影響を与えるほどではないことを示唆しています。しかし、このような小さな最適化の積み重ねが、コンパイラが生成するコード全体の品質向上に寄与します。
コアとなるコードの変更箇所
変更は src/cmd/5g/cgen.c
ファイルの abop:
ラベルが付いたコードブロック内で行われています。
--- a/src/cmd/5g/cgen.c
+++ b/src/cmd/5g/cgen.c
@@ -439,18 +439,43 @@ abop: // asymmetric binary
if(nl->ullman >= nr->ullman) {
regalloc(&n1, nl->type, res);
cgen(nl, &n1);
- regalloc(&n2, nr->type, N);
- cgen(nr, &n2);
+ switch(n->op) {
+ case OADD:
+ case OSUB:
+ case OAND:
+ case OOR:
+ case OXOR:
+ if(smallintconst(nr)) {
+ n2 = *nr;
+ break;
+ }
+ default:
+ regalloc(&n2, nr->type, N);
+ cgen(nr, &n2);
+ }
} else {
- regalloc(&n2, nr->type, res);
- cgen(nr, &n2);
+ switch(n->op) {
+ case OADD:
+ case OSUB:
+ case OAND:
+ case OOR:
+ case OXOR:
+ if(smallintconst(nr)) {
+ n2 = *nr;
+ break;
+ }
+ default:
+ regalloc(&n2, nr->type, res);
+ cgen(nr, &n2);
+ }
regalloc(&n1, nl->type, N);
cgen(nl, &n1);
}
gins(a, &n2, &n1);
gmove(&n1, res);
regfree(&n1);
- regfree(&n2);
+ if(n2.op != OLITERAL)
+ regfree(&n2);
goto ret;
flt: // floating-point.
コアとなるコードの解説
この変更の核心は、二項演算の右オペランド (nr
) が特定の条件を満たす場合に、レジスタ割り当てとコード生成のプロセスをスキップし、直接リテラル値として扱うようにした点です。
-
switch(n->op)
ブロックの導入:- 変更前は、左右のオペランドのUllman数に基づいて、一律に
regalloc
とcgen
を呼び出してn2
(右オペランドのコード生成結果) をレジスタにロードしていました。 - 変更後では、
n->op
(現在のノードの演算の種類) がOADD
,OSUB
,OAND
,OOR
,OXOR
のいずれかである場合に、特別な処理を行うswitch
文が導入されました。これらの演算は、ARMのADD
命令などで即値オペランドを直接サポートする可能性が高いものです。
- 変更前は、左右のオペランドのUllman数に基づいて、一律に
-
smallintconst(nr)
のチェックと最適化:if(smallintconst(nr))
の条件が追加されました。このsmallintconst
関数は、nr
がARM命令のOperand2
として直接エンコード可能な「小さな整数定数」であるかを判定します。- もし条件が真であれば、
n2 = *nr;
とすることで、nr
ノード自体をn2
に代入します。これにより、n2
はレジスタを指すものではなく、コンパイラ内部でリテラル値として扱われるようになります。このパスでは、regalloc
とcgen
は呼び出されません。 default
ケースでは、以前と同様にregalloc
とcgen
を呼び出して、nr
をレジスタにロードします。これは、nr
が定数ではない場合や、Operand2
として直接エンコードできない大きな定数である場合に適用されます。
-
regfree(&n2)
の条件付き実行:- コードブロックの最後にある
regfree(&n2);
の呼び出しがif(n2.op != OLITERAL) regfree(&n2);
に変更されました。 - これは、
n2
がOLITERAL
(リテラルノード) である場合、つまり上記最適化パスでレジスタが割り当てられなかった場合は、解放すべきレジスタが存在しないため、regfree
を呼び出す必要がないことを意味します。これにより、不要な処理が削減されます。
- コードブロックの最後にある
この一連の変更により、Goコンパイラは、ARMアーキテクチャの特性を活かして、特定の定数を含む二項演算において、より効率的で命令数の少ないアセンブリコードを生成できるようになりました。
関連リンク
- Go CL (Change List): https://golang.org/cl/6564067
参考にした情報源リンク
- Go Compiler Internals (古い情報ですが、コンパイラの基本的な概念理解に役立ちます): https://go.dev/doc/articles/go_compiler_internals.html
- ARM Architecture Reference Manual (ARM命令セット、特にOperand2に関する詳細): 一般的にARMの公式ドキュメントや、関連する技術書・オンラインリソースを参照します。例えば、"ARM Architecture Reference Manual" や "ARM Assembly Language Programming" などのキーワードで検索すると良いでしょう。
- Go言語のソースコード:
src/cmd/5g/cgen.c
および関連ファイル。