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

[インデックス 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 のような二項演算において、オペランド AB の性質が異なる場合に適用される最適化の概念です。例えば、一方がレジスタに格納された変数で、もう一方が定数である場合などが典型的なケースです。このような場合、コンパイラは両方のオペランドを同じように扱うのではなく、定数オペランドの特性を活かした特別なコード生成パスを選択できます。

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) のコードを生成する際、左右のオペランド nlnr のUllman数(レジスタ必要数を示すヒューリスティック値)を比較し、より多くのレジスタを必要とする側から処理を開始していました。どちらのオペランドも、regalloc (レジスタ割り当て) と cgen (コード生成) を通じて、最終的にレジスタにロードされていました。

変更後のコードでは、特に OADD (加算), OSUB (減算), OAND (論理AND), OOR (論理OR), OXOR (論理XOR) といった特定の二項演算において、右オペランド (nr) が「小さな整数定数 (smallintconst)」である場合に、特別な処理を行うようになりました。

  1. smallintconst(nr) のチェック: この関数(コミットの差分には含まれていませんが、その存在が示唆されています)は、nr がARMの Operand2 として直接エンコード可能な形式の小さな整数定数であるかを判定します。例えば、4 のような値は、追加の MOVW 命令なしに ADD 命令の即値として直接埋め込むことができます。
  2. 一時レジスタの回避:
    • もし nrsmallintconst であれば、n2 = *nr; のように、nr のノード自体を n2 にコピーします。この場合、regalloc(&n2, nr->type, N);cgen(nr, &n2); の呼び出しはスキップされます。これにより、定数をレジスタにロードするための MOVW 命令が不要になります。
    • n2 はもはやレジスタを指すものではなく、直接命令に埋め込まれるリテラル値として扱われます。
  3. 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)を加算

ここでは、定数 4R1 という一時レジスタにロードするために 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) が特定の条件を満たす場合に、レジスタ割り当てとコード生成のプロセスをスキップし、直接リテラル値として扱うようにした点です。

  1. switch(n->op) ブロックの導入:

    • 変更前は、左右のオペランドのUllman数に基づいて、一律に regalloccgen を呼び出して n2 (右オペランドのコード生成結果) をレジスタにロードしていました。
    • 変更後では、n->op (現在のノードの演算の種類) が OADD, OSUB, OAND, OOR, OXOR のいずれかである場合に、特別な処理を行う switch 文が導入されました。これらの演算は、ARMの ADD 命令などで即値オペランドを直接サポートする可能性が高いものです。
  2. smallintconst(nr) のチェックと最適化:

    • if(smallintconst(nr)) の条件が追加されました。この smallintconst 関数は、nr がARM命令の Operand2 として直接エンコード可能な「小さな整数定数」であるかを判定します。
    • もし条件が真であれば、n2 = *nr; とすることで、nr ノード自体を n2 に代入します。これにより、n2 はレジスタを指すものではなく、コンパイラ内部でリテラル値として扱われるようになります。このパスでは、regalloccgen は呼び出されません。
    • default ケースでは、以前と同様に regalloccgen を呼び出して、nr をレジスタにロードします。これは、nr が定数ではない場合や、Operand2 として直接エンコードできない大きな定数である場合に適用されます。
  3. regfree(&n2) の条件付き実行:

    • コードブロックの最後にある regfree(&n2); の呼び出しが if(n2.op != OLITERAL) regfree(&n2); に変更されました。
    • これは、n2OLITERAL (リテラルノード) である場合、つまり上記最適化パスでレジスタが割り当てられなかった場合は、解放すべきレジスタが存在しないため、regfree を呼び出す必要がないことを意味します。これにより、不要な処理が削減されます。

この一連の変更により、Goコンパイラは、ARMアーキテクチャの特性を活かして、特定の定数を含む二項演算において、より効率的で命令数の少ないアセンブリコードを生成できるようになりました。

関連リンク

参考にした情報源リンク

  • 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 および関連ファイル。