[インデックス 1241] ファイルの概要
このコミットは、Go言語の初期のコンパイラバックエンドの一つである src/cmd/6g/gen.c ファイルに対する変更です。6g は、Go言語のコンパイラがx86-64アーキテクチャ向けにコードを生成する際に使用されるバックエンドでした。gen.c は、このバックエンドにおいて、Go言語の抽象構文木 (AST) を具体的な機械語命令に変換する、いわゆるコード生成 (code generation) の主要な部分を担っています。特に、このファイルは様々な演算子や式のコード生成ロジックを含んでいます。
コミット
commit 9a5c7eab16528cd6b83a7f12b7eb04188e93a857
Author: Ken Thompson <ken@golang.org>
Date: Mon Nov 24 17:51:26 2008 -0800
better code for += -= ^= |= and &=
R=r
OCL=19953
CL=19953
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9a5c7eab16528cd6b83a7f12b7eb04188e93a857
元コミット内容
better code for += -= ^= |= and &=
R=r
OCL=19953
CL=19953
変更の背景
このコミットは、Go言語の初期開発段階、特にGoコンパイラの最適化フェーズにおける改善の一環として行われました。Go言語は、その設計思想として「シンプルさ」と「効率性」を重視しており、コンパイラもまた生成されるコードの品質と実行速度に重点を置いていました。
+=, -=, ^=, |=, &= といった複合代入演算子は、プログラミングにおいて頻繁に使用されます。これらの演算子は、a = a + b のような形式を a += b と短縮して記述できる糖衣構文 (syntactic sugar) ですが、コンパイラにとっては、より効率的な機械語命令に変換する機会を提供します。
変更前は、これらの複合代入演算子、特に += 1 や -= 1 のようなケースにおいて、コンパイラが必ずしも最適な機械語命令を生成していなかった可能性があります。例えば、a = a + 1 は通常、アセンブリ言語では ADD 命令で実装されますが、より効率的な INC (インクリメント) 命令が存在します。同様に a = a - 1 には DEC (デクリメント) 命令があります。ビット演算子についても、より直接的な命令に変換することで、生成されるコードのサイズを小さくし、実行速度を向上させることが期待されます。
このコミットは、Goコンパイラがこれらの複合代入演算子に対して、より「良いコード」を生成するように、つまりより最適化された機械語命令(特に INC や DEC 命令、および直接的なビット演算命令)を利用するように改善することを目的としています。これは、Go言語のパフォーマンス目標を達成するための、初期の重要なステップの一つでした。
前提知識の解説
Go言語の初期開発とコンパイラ
Go言語は、GoogleでKen Thompson、Rob Pike、Robert Griesemerによって設計され、2009年にオープンソースとして公開されました。その設計目標の一つに、大規模なソフトウェア開発における生産性の向上と、C言語に匹敵する実行速度の実現がありました。
Goコンパイラは、当初から独自のツールチェインを持っていました。C言語で書かれたコンパイラ(6g, 8g, 5g など)が各アーキテクチャ(x86-64, x86, ARMなど)向けに存在し、それぞれがGoソースコードを対応するアセンブリコードに変換していました。
src/cmd/6g
src/cmd/6g は、Go言語の初期のコンパイラツールチェインの一部であり、特に x86-64 (64-bit Intel/AMD) アーキテクチャ向けのコンパイラバックエンドでした。Goソースコードを解析し、中間表現を経て、最終的にx86-64のアセンブリコードを生成する役割を担っていました。
gen.c
gen.c は、6g コンパイラの中で「コード生成 (code generation)」を担当するファイルです。Go言語のソースコードは、まず字句解析、構文解析を経て抽象構文木 (AST) に変換されます。gen.c は、このASTを受け取り、それをターゲットアーキテクチャ(この場合はx86-64)の具体的な機械語命令(アセンブリ命令)に変換するロジックを含んでいます。変数のロード/ストア、算術演算、論理演算、制御フローなど、あらゆるGo言語の構文要素に対応するアセンブリコードを生成します。
複合代入演算子 (Compound Assignment Operators)
複合代入演算子とは、算術演算子やビット演算子と代入演算子 (=) を組み合わせたものです。例えば、a = a + b は a += b と書くことができます。
+=(加算代入):a += bはa = a + bと同じ。-=(減算代入):a -= bはa = a - bと同じ。*=(乗算代入):a *= bはa = a * bと同じ。/=(除算代入):a /= bはa = a / bと同じ。%=(剰余代入):a %= bはa = a % bと同じ。&=(ビットAND代入):a &= bはa = a & bと同じ。|=(ビットOR代入):a |= bはa = a | bと同じ。^=(ビットXOR代入):a ^= bはa = a ^ bと同じ。<<=(左シフト代入):a <<= bはa = a << bと同じ。>>=(右シフト代入):a >>= bはa = a >> bと同じ。
これらの演算子は、コードの簡潔さを向上させるだけでなく、コンパイラが特定の最適化を行う機会を提供します。
アセンブリ言語と最適化
アセンブリ言語は、CPUが直接実行できる機械語命令を人間が読める形式で記述したものです。コンパイラは、高水準言語(Goなど)のコードをアセンブリ言語に変換し、最終的に機械語に変換します。
最適化とは、コンパイラが生成するコードの効率(実行速度、メモリ使用量、コードサイズなど)を向上させるプロセスです。このコミットで言及されている最適化は、特に以下の点に関連します。
INC(Increment) とDEC(Decrement) 命令: x86系のCPUには、レジスタやメモリの値を1だけ増やす (INC) または減らす (DEC) 専用の命令があります。これらの命令は、一般的なADDやSUB命令よりも高速で、コードサイズも小さい場合があります。例えば、ADD EAX, 1よりもINC EAXの方が効率的です。- ビット演算命令:
AND,OR,XORなどのビット演算も、CPUには専用の命令が存在します。これらを直接利用することで、効率的なコードを生成できます。 - レジスタ割り当て: CPUのレジスタは非常に高速な記憶領域です。コンパイラは、頻繁にアクセスされる値をレジスタに割り当てることで、メモリへのアクセスを減らし、パフォーマンスを向上させます。
regalloc(register allocate) はレジスタを割り当てる処理、regfree(register free) は割り当てたレジスタを解放する処理を指します。
このコミットは、これらのアセンブリレベルの最適化をGoコンパイラに導入し、生成されるコードの品質を高めることを目的としています。
技術的詳細
このコミットは、src/cmd/6g/gen.c 内の cgen_asop 関数に焦点を当てています。cgen_asop 関数は、Go言語の複合代入演算子(例: +=, -=, ^=, |=, &=)のコード生成を担当しています。
変更の核心は、特定の複合代入演算子、特に += 1 や -= 1 のようなケースに対して、より効率的なアセンブリ命令である INC (インクリメント) および DEC (デクリメント) を生成するように改善した点です。また、ビット演算子 (^=, |=, &=) のコード生成ロジックも改善されています。
変更前の挙動
変更前のコードでは、OADD (加算) や OSUB (減算) の複合代入において、右辺の値が 1 であっても、goto com; を使って一般的な複合代入の処理にフォールバックしていました。com: ラベル以下のコードは、OXOR, OAND, OOR といったビット演算子を処理する部分であり、一般的な ADD や SUB 命令を生成していました。これは、a += 1 のような単純なケースでも、INC や DEC 命令のような最適化された命令が使われず、効率が悪い可能性がありました。
具体的には、OADD や OSUB のケースで、右辺が 1 であっても goto com; にジャンプし、結果的に gins(optoas(n->etype, nl->type), nr, nl); のような汎用的なコード生成パスを通っていました。これは、ADD や SUB 命令を生成することになります。
変更後の挙動
変更後のコードでは、OADD と OSUB のケースで、右辺の値が 1 である場合に INC または DEC 命令を直接生成するようにロジックが変更されました。
-
OADD(加算代入):- 左辺 (
nl) が整数型であり、かつ右辺 (nr) の値が1である場合、gins(optoas(OINC, nl->type), N, nl);を呼び出してINC命令を生成します。 - それ以外の場合は
break;して、後続の汎用的な複合代入処理に進みます。
- 左辺 (
-
OSUB(減算代入):- 左辺 (
nl) が整数型であり、かつ右辺 (nr) の値が1である場合、gins(optoas(ODEC, nl->type), N, nl);を呼び出してDEC命令を生成します。 - それ以外の場合は
break;して、後続の汎用的な複合代入処理に進みます。
- 左辺 (
この変更により、x += 1 は INC x に、x -= 1 は DEC x に変換されるようになり、より効率的なアセンブリコードが生成されます。
ビット演算子と汎用複合代入の改善
変更後のコードでは、if(nl->addable) という条件が追加され、左辺がアドレス指定可能な場合にのみ、以下の複合代入演算子の最適化パスに入ります。
OXOR(ビットXOR代入)OAND(ビットAND代入)OOR(ビットOR代入)OADD(加算代入) -+= 1/-= 1以外のケースOSUB(減算代入) -+= 1/-= 1以外のケース
これらの演算子に対しては、以下の手順でコードが生成されます。
- レジスタ割り当て:
regalloc(&n2, nr->type, N);を呼び出し、右辺 (nr) の値を一時的に保持するためのレジスタn2を割り当てます。 - 右辺のコード生成:
cgen(nr, &n2);を呼び出し、右辺の値をn2にロードするコードを生成します。 - 複合代入命令の生成:
gins(optoas(n->etype, nl->type), &n2, nl);を呼び出し、n2の値と左辺 (nl) の値を使って、対応する複合代入命令(例:XOR,AND,OR,ADD,SUB)を生成します。 - レジスタ解放:
regfree(&n2);を呼び出し、一時的に使用したレジスタn2を解放します。
このアプローチにより、右辺が複雑な式である場合でも、その値を一時レジスタにロードしてから演算を行うことで、より堅牢で効率的なコード生成が可能になります。特に、nl->addable のチェックは、左辺がメモリ上の変数である場合に、直接メモリに対する演算を行うのではなく、レジスタを介して演算を行うことで、より効率的なコードパスを選択していることを示唆しています。
全体として、このコミットはGoコンパイラのコード生成ロジックを洗練させ、頻繁に使用される複合代入演算子に対して、より最適化されたアセンブリ命令を生成することで、Goプログラムの実行効率を向上させています。
コアとなるコードの変更箇所
--- a/src/cmd/6g/gen.c
+++ b/src/cmd/6g/gen.c
@@ -921,26 +921,35 @@ cgen_asop(Node *n)
switch(n->etype) {
case OADD:
if(!isint[nl->type->etype])
- goto com;
+ break;
if(mpgetfix(nr->val.u.xval) != 1)
- goto com;
+ break;
gins(optoas(OINC, nl->type), N, nl);
goto ret;
case OSUB:
if(!isint[nl->type->etype])
- goto com;
+ break;
if(mpgetfix(nr->val.u.xval) != 1)
- goto com;
+ break;
gins(optoas(ODEC, nl->type), N, nl);
goto ret;
+ }
- com:
+ if(nl->addable)
+ switch(n->etype) {
case OXOR:
case OAND:
case OOR:
+ case OADD:
+ case OSUB:
if(!isint[nl->type->etype])
break;
- gins(optoas(n->etype, nl->type), nr, nl);
+ if(!isint[nr->type->etype])
+ break;
+ regalloc(&n2, nr->type, N);
+ cgen(nr, &n2);
+ gins(optoas(n->etype, nl->type), &n2, nl);
+ regfree(&n2);
goto ret;
}
コアとなるコードの解説
このdiffは、src/cmd/6g/gen.c 内の cgen_asop 関数における複合代入演算子のコード生成ロジックの変更を示しています。
-
OADDとOSUBの最適化パスの変更:- 変更前は、
OADD(加算代入) とOSUB(減算代入) のケースで、右辺が1であってもgoto com;にジャンプしていました。これは、INCやDEC命令のような最適化された命令ではなく、汎用的なADDやSUB命令を生成するパスにフォールバックすることを意味していました。 - 変更後、
goto com;がbreak;に置き換えられました。これにより、OADDやOSUBのケースで、左辺が整数型で、かつ右辺が1である場合に、直接gins(optoas(OINC, nl->type), N, nl);またはgins(optoas(ODEC, nl->type), N, nl);を呼び出すようになりました。これは、それぞれINC(インクリメント) およびDEC(デクリメント) アセンブリ命令を生成するためのものです。これにより、x += 1やx -= 1のようなコードがより効率的な機械語に変換されるようになります。
- 変更前は、
-
汎用複合代入処理の改善と
nl->addableの導入:- 変更前は、
com:ラベル以下でOXOR,OAND,OORの複合代入を処理していました。このパスはgins(optoas(n->etype, nl->type), nr, nl);を使用しており、右辺 (nr) の値を直接演算命令に渡していました。 - 変更後、
com:ラベルが削除され、代わりにif(nl->addable)という条件が追加されました。この条件は、左辺 (nl) がアドレス指定可能(つまり、メモリ上に存在し、直接アクセスできる)である場合に真となります。 - この
ifブロック内には、OXOR,OAND,OORに加えて、OADDとOSUBも含まれるようになりました。これは、+= 1や-= 1の最適化パスで処理されなかった、より一般的な+=や-=のケース(例:x += yやx -= 5など)をここで処理することを示しています。 - この新しい汎用パスでは、以下の手順が導入されました。
regalloc(&n2, nr->type, N);: 右辺 (nr) の値を一時的に保持するための新しいノードn2(通常はレジスタ) を割り当てます。cgen(nr, &n2);: 右辺の式nrのコードを生成し、その結果をn2に格納します。これにより、右辺が複雑な式であっても、その評価結果が一時レジスタにロードされます。gins(optoas(n->etype, nl->type), &n2, nl);:n2に格納された右辺の値と左辺nlを使って、実際の複合代入演算(例:XOR,AND,OR,ADD,SUB)のアセンブリ命令を生成します。regfree(&n2);: 使用した一時レジスタn2を解放します。
- 変更前は、
この変更により、コンパイラは複合代入演算子に対して、より状況に応じた最適なコードを生成できるようになりました。特に、+= 1 や -= 1 のような頻繁に使われるケースでは INC/DEC 命令による高速化が図られ、その他の一般的なケースでは、右辺の評価結果を一時レジスタにロードしてから演算を行うことで、より堅牢で効率的なコード生成パスが確立されました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のコンパイラに関する情報 (Go Wiki): https://go.dev/wiki/Compiler
- Go言語の初期のソースコード (GitHub): https://github.com/golang/go/tree/release-branch.go1 (このコミットはGo 1リリース前の非常に初期のものです)
参考にした情報源リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/9a5c7eab16528cd6b83a7f12b7eb04188e93a857
- Go言語の歴史と設計に関する情報 (Go Blogなど)
- x86アセンブリ言語の命令セットに関する一般的な知識