[インデックス 15115] ファイルの概要
このコミットは、Goコンパイラのコード生成における最適化に関するものです。具体的には、レジスタ割り当てと不要なゼロ初期化の削除を通じて、生成されるコードの効率を向上させています。
コミット
commit 2c09d6992f7a13d680ce8f3a0f19366dfcc93713
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 3 14:51:21 2013 -0500
cmd/gc: slightly better code generation
* Avoid treating CALL fn(SB) as justification for introducing
and tracking a registerized variable for fn(SB).
* Remove USED(n) after declaration and zeroing of n.
It was left over from when the compiler emitted more
aggressive set and not used errors, and it was keeping
the optimizer from removing a redundant zeroing of n
when n was a pointer or integer variable.
Update #597.
R=ken2
CC=golang-dev
https://golang.org/cl/7277048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2c09d6992f7a13d680ce8f3a0f19366dfcc93713
元コミット内容
このコミットは、Goコンパイラ(cmd/gc
)におけるコード生成の改善を目的としています。主な変更点は以下の2点です。
CALL fn(SB)
のような直接呼び出し関数に対して、レジスタ化された変数を導入し追跡する処理を回避するようになりました。これにより、不要なレジスタ割り当てを削減します。- 変数の宣言とゼロ初期化の後に行われていた
USED(n)
の呼び出しを削除しました。これは、以前のコンパイラがより厳密な「設定されたが使用されていない」エラーを出力していた頃の名残であり、ポインタや整数変数の冗長なゼロ初期化をオプティマイザが削除するのを妨げていました。
この変更は、Go issue #597 に関連しています。
変更の背景
Goコンパイラは、生成されるバイナリのサイズと実行速度を最適化するために継続的に改善されています。このコミットは、特に以下の問題に対処しています。
- 不要なレジスタ割り当て: Goコンパイラは、関数呼び出しの際に、呼び出される関数(
fn(SB)
)をレジスタに割り当てて追跡することがありました。しかし、fn(SB)
が直接呼び出される外部シンボルである場合、その関数自体をレジスタ変数として扱う必要はありません。このような不要なレジスタ割り当ては、コード生成の効率を低下させ、場合によってはレジスタの競合を引き起こす可能性がありました。 - 冗長なゼロ初期化: Go言語では、変数は宣言時にゼロ値で初期化されることが保証されています。しかし、コンパイラの内部処理において、変数が宣言されゼロ初期化された後にも
USED(n)
というマーカーが残っていることがありました。このUSED(n)
は、変数が「使用されている」とコンパイラに誤って認識させ、結果としてオプティマイザが冗長なゼロ初期化(特にポインタや整数型の場合)を削除するのを妨げていました。これは、以前のコンパイラが「設定されたが使用されていない」変数に対してより厳格なエラーチェックを行っていた時代の遺産であり、現代の最適化パスには不要なものでした。
これらの非効率性を解消することで、Goコンパイラはより小さく、より高速なバイナリを生成できるようになります。
前提知識の解説
このコミットを理解するためには、Goコンパイラの内部構造と、一般的なコンパイラ最適化の概念に関する知識が必要です。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラは、cmd/gc
というディレクトリに存在します。これは、Goソースコードを各アーキテクチャ(例:5g
はARM、6g
はx86-64、8g
はx86)向けの機械語に変換する役割を担っています。 - レジスタ割り当て (Register Allocation): コンパイラの重要な最適化フェーズの一つで、プログラムの変数をCPUの高速なレジスタに割り当てるプロセスです。レジスタに割り当てられた変数は、メモリから読み書きするよりもはるかに高速にアクセスできます。しかし、レジスタの数は限られているため、効率的な割り当て戦略が求められます。不要なレジスタ割り当ては、レジスタの枯渇やスピル(レジスタからメモリへの退避)を引き起こし、性能を低下させます。
fn(SB)
: Goコンパイラにおけるシンボル参照の表記法です。fn
は関数名、SB
は「Static Base」を意味し、グローバルシンボルや外部シンボルを参照する際に使用されます。CALL fn(SB)
は、特定の外部関数を直接呼び出すアセンブリ命令に相当します。- ゼロ初期化 (Zero Initialization): Go言語の仕様では、変数は宣言時にその型のゼロ値で自動的に初期化されます(例: 整数は0、ポインタはnil、ブール値はfalse)。これは、未初期化変数によるバグを防ぐための重要な機能です。
- オプティマイザ (Optimizer): コンパイラの一部で、生成されるコードの性能(速度、サイズなど)を向上させるための変換を行うモジュールです。冗長なコードの削除、命令の並べ替え、レジスタの最適化など、様々な手法を用います。
USED(n)
: Goコンパイラの内部で使われるマクロまたは関数で、特定のノード(変数など)がコード内で「使用されている」ことをマークするために使われていました。これは、コンパイラがデッドコード(使用されていないコード)を検出したり、「設定されたが使用されていない」変数を警告したりする際に利用される情報の一部でした。しかし、このコミットの時点では、その役割が変化し、むしろ最適化の妨げになっていたことが示唆されています。- Go Issue #597: Go言語の公式イシュートラッカーで管理されている特定のバグ報告または機能改善要求です。このコミットがこのイシューを解決したことを示しています。
技術的詳細
このコミットは、Goコンパイラのバックエンド、特にレジスタ割り当てとコード生成のフェーズに影響を与えます。
-
CALL fn(SB)
の扱い:- Goコンパイラのレジスタ割り当てロジック(
regopt
関数)は、プログラムの命令ストリームを走査し、どの変数がレジスタに割り当てられるべきかを決定します。 - 以前は、
ABL
(ARM) やACALL
(x86/x86-64) といった直接関数呼び出し命令(p->as == ABL
またはp->as == ACALL
)で、かつ呼び出し先が外部シンボル(p->to.type == D_EXTERN
)である場合でも、その関数シンボル自体をレジスタ化された変数として追跡しようとすることがありました。 - このコミットでは、
regopt
関数内でこのような命令を検出した場合に、continue
ステートメントを使ってその命令の処理をスキップするように変更されました。これにより、コンパイラは直接呼び出される外部関数に対して不要なレジスタ変数を導入しなくなり、レジスタ割り当ての効率が向上します。これは、関数自体がレジスタにロードされる必要がないため、レジスタを他の用途に解放できることを意味します。
- Goコンパイラのレジスタ割り当てロジック(
-
USED(n)
の削除:src/cmd/gc/gen.c
は、GoのAST(抽象構文木)から中間コードを生成する部分です。cgen_as
関数は、代入操作(nl = nr
)を処理します。- 以前のコードでは、
clearslim(nl)
(おそらくノードnl
に関連する一時的な状態をクリアする関数)の後に、nl
がaddable
(アドレス指定可能、つまりメモリ上に存在しうる)であればgused(nl)
(USED(n)
に相当する内部関数)を呼び出していました。 - この
gused(nl)
の呼び出しは、変数が宣言されゼロ初期化された直後であっても、その変数が「使用されている」とマークしていました。 - このコミットでは、この
gused(nl)
の呼び出しが完全に削除されました。これにより、コンパイラのオプティマイザは、変数が実際に使用される前に冗長なゼロ初期化が行われている場合に、その初期化を削除できるようになります。例えば、var x int
と宣言され、すぐにx = 10
と代入されるような場合、x
の初期ゼロ値への設定は不要であり、オプティマイザがこれを認識して削除できるようになります。これは、特にポインタや整数型のような、ゼロ初期化がメモリ書き込みを伴う場合にパフォーマンス上の利点があります。
これらの変更は、Goコンパイラのバックエンドにおけるマイクロ最適化であり、全体的なコード生成品質の向上に寄与します。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の4つのファイルにわたっています。
src/cmd/5g/reg.c
(ARMアーキテクチャ向けレジスタ割り当て)src/cmd/6g/reg.c
(x86-64アーキテクチャ向けレジスタ割り当て)src/cmd/8g/reg.c
(x86アーキテクチャ向けレジスタ割り当て)src/cmd/gc/gen.c
(Goコンパイラの汎用コード生成)
src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
の変更
これらのファイルでは、regopt
関数内に以下のコードが追加されています。
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -275,6 +275,10 @@ regopt(Prog *firstp)\n \t\t\t}\n \t\t}\n \n+\t\t// Avoid making variables for direct-called functions.\n+\t\tif(p->as == ABL && p->to.type == D_EXTERN)\n+\t\t\tcontinue;\n+\n \t\t/*
\t\t * left side always read
\t\t */
(6g
と8g
ではABL
の代わりにACALL
が使用されていますが、ロジックは同じです。)
src/cmd/gc/gen.c
の変更
このファイルでは、cgen_as
関数内から以下の2行が削除されています。
--- a/src/cmd/gc/gen.c
+++ b/src/cmd/gc/gen.c
@@ -735,8 +735,6 @@ cgen_as(Node *nl, Node *nr)\n \t\t\treturn;\n \t\t}\n \t\tclearslim(nl);\n-\t\tif(nl->addable)\n-\t\t\tgused(nl);\
\t\treturn;\
\t}\
\
コアとなるコードの解説
reg.c
ファイル群の変更 (5g
, 6g
, 8g
)
これらのファイルは、Goコンパイラの各アーキテクチャ固有のレジスタ割り当てパスを実装しています。regopt
関数は、アセンブリ命令のリスト(Prog
構造体で表現される)を走査し、レジスタの使用状況を最適化します。
追加されたコードブロックは以下の通りです。
// Avoid making variables for direct-called functions.
if(p->as == ABL && p->to.type == D_EXTERN)
continue;
p->as
: 現在処理しているアセンブリ命令の種類を示します。ABL
(Branch and Link) はARMアーキテクチャでの関数呼び出し命令、ACALL
はx86/x86-64アーキテクチャでの関数呼び出し命令です。p->to.type == D_EXTERN
: 命令のターゲット(p->to
)が外部シンボル(D_EXTERN
)であることを示します。これは、Goプログラム内で定義されていない、外部ライブラリやシステムコールなどの関数を指します。continue;
: この条件が真の場合、現在の命令に対する残りのレジスタ割り当て処理をスキップし、次の命令の処理に移ります。
この変更により、コンパイラは直接呼び出される外部関数(例: runtime.newobject(SB)
のようなランタイム関数)を、レジスタ割り当ての対象となる「変数」として誤って扱わなくなります。これにより、レジスタ割り当てのオーバーヘッドが減少し、より効率的なコードが生成されます。
gen.c
ファイルの変更
src/cmd/gc/gen.c
は、Goのソースコードから中間表現を生成する際の汎用的な処理を含んでいます。cgen_as
関数は、代入文(例: x = y
)を処理する際に呼び出されます。
削除されたコードブロックは以下の通りです。
if(nl->addable)
gused(nl);
nl
: 代入の左辺(LHS)を表すノードです。nl->addable
:nl
がアドレス指定可能(つまり、メモリ上に実体を持つことができる)な変数であることを示すフラグです。gused(nl)
: ノードnl
が「使用された」ことをコンパイラに通知する内部関数です。
このgused(nl)
の呼び出しは、変数が宣言されゼロ初期化された直後であっても、その変数が「使用された」とマークしていました。しかし、変数が実際に値が代入される前に使用されることは稀であり、このマークはオプティマイザが冗長なゼロ初期化を削除するのを妨げていました。
この行を削除することで、コンパイラは変数の初期ゼロ値への設定が不要な場合に、その操作を最適化して削除できるようになります。例えば、var x int; x = 10
というコードがあった場合、x
が0
で初期化される処理は、その直後に10
が代入されるため冗長です。この変更により、コンパイラはこのような冗長な初期化を認識し、生成される機械語から取り除くことが可能になります。
関連リンク
- Go Issue 597: https://code.google.com/p/go/issues/detail?id=597 (古いGoogle Codeのリンクですが、Goのイシュートラッカーの歴史的な記録です)
- Go CL 7277048: https://golang.org/cl/7277048 (Goの変更リスト、このコミットに対応するレビューページ)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード
- コンパイラ最適化に関する一般的な情報源 (レジスタ割り当て、デッドコード削除など)
- Go Issue Tracker (Google Code Archive)
- Go Code Review (Gerrit)
- Go言語のコンパイラに関するブログ記事や解説記事 (一般的な知識として)