[インデックス 14587] ファイルの概要
コミット
このコミットは、Goコンパイラのレジスタ最適化フェーズ(regopt
)において、レジスタを表すために使用されるONAME
ノードを再利用するように変更を加えるものです。これにより、メモリ使用量が約5%削減されると報告されています。
- コミットハッシュ:
45fe306ac85dae6fa599b51f215090b73cf75bad
- 作者: Rémy Oudompheng
- 日付: 2012年12月9日 日曜日 19:10:52 +0100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/45fe306ac85dae6fa599b51f215090b73cf75bad
元コミット内容
cmd/[568]g: recycle ONAME nodes used in regopt to denote registers.
The reported decrease in memory usage is about 5%.
R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/6902064
変更の背景
Goコンパイラは、ソースコードを機械語に変換する過程で、様々な中間表現(IR)を生成し、それらを操作します。この過程で、多くのデータ構造(ノード)がメモリ上に作成されます。特に、レジスタ割り当てや最適化のフェーズでは、一時的な変数やレジスタを表すために特定のノードが頻繁に生成されることがあります。
このコミットが行われた時点では、Goコンパイラのレジスタ最適化フェーズ(regopt
)において、レジスタを表すONAME
ノードが毎回新しく生成されていました。これは、コンパイルのたびに同じ意味を持つノードが重複して作成されることを意味し、結果としてメモリの無駄遣いにつながっていました。特に大規模なコードベースをコンパイルする際には、このメモリ消費が無視できないレベルに達する可能性がありました。
この非効率性を解消し、コンパイラのメモリフットプリントを削減することが、この変更の主な背景です。メモリ使用量を削減することは、コンパイル時間の短縮や、リソースが限られた環境でのコンパイラの実行可能性向上に寄与します。
前提知識の解説
Goコンパイラの構造 (cmd/5g
, 6g
, 8g
)
Go言語のコンパイラは、ターゲットアーキテクチャごとに異なる実装を持っています。コミットメッセージにあるcmd/[568]g
は、以下のコンパイラを指します。
cmd/5g
: ARMアーキテクチャ向けのGoコンパイラ。cmd/6g
: x86-64 (AMD64) アーキテクチャ向けのGoコンパイラ。cmd/8g
: x86 (IA-32) アーキテクチャ向けのGoコンパイラ。
これらのコンパイラは、それぞれ特定のアーキテクチャに特化したコード生成や最適化ロジックを含んでいますが、多くの共通のフロントエンドおよび中間処理ロジックを共有しています。このコミットがreg.c
という共通のファイルに影響を与えているのは、レジスタ最適化の基本的なアプローチが各アーキテクチャで共通しているためです。
ONAME
ノードとは何か
Goコンパイラ内部では、ソースコードの要素(変数、関数、型など)は抽象構文木(AST)のノードとして表現されます。ONAME
ノードは、ASTにおける「名前」を表すノードの一種です。これは、変数名、関数名、あるいはコンパイラが内部的に使用するシンボルなど、識別子全般を指すことができます。
レジスタ最適化の文脈では、ONAME
ノードは特定の物理レジスタ(例: AX
, BX
, SP
など)を抽象的に表現するために使用されることがあります。これにより、コンパイラはレジスタを通常の変数と同様に扱うことができ、最適化アルゴリズムを適用しやすくなります。
regopt
(レジスタ最適化) の役割
regopt
は「レジスタ最適化(Register Optimization)」の略で、Goコンパイラのバックエンドにおける重要なフェーズの一つです。このフェーズの主な目的は、プログラムの実行速度を向上させるために、頻繁にアクセスされる変数や中間結果をCPUの高速なレジスタに割り当てることです。
レジスタはメモリよりもはるかに高速にアクセスできるため、データをレジスタに保持することで、メモリへのアクセス回数を減らし、プログラムのパフォーマンスを大幅に改善できます。regopt
は、データフロー解析などを用いて、どの変数をどのレジスタに割り当てるべきかを決定します。このプロセスでは、レジスタの競合を解決したり、レジスタの使用効率を最大化したりするための複雑なアルゴリズムが用いられます。
コンパイラのメモリ管理とノードの再利用の重要性
コンパイラは、大規模なプログラムを処理する際に、非常に多くのデータ構造をメモリ上に構築します。これらのデータ構造は、AST、シンボルテーブル、中間コードなど多岐にわたります。コンパイルプロセス全体を通して、これらのデータ構造の生成、変更、破棄が繰り返されます。
効率的なメモリ管理は、コンパイラのパフォーマンスと安定性にとって極めて重要です。
- メモリ使用量の削減: 不要なオブジェクトの生成を避け、既存のオブジェクトを再利用することで、コンパイラが消費するメモリ量を減らすことができます。これは、特にメモリが限られたシステムや、多数のコンパイルジョブを並行して実行するCI/CD環境において重要です。
- ガベージコレクションの負荷軽減: Goのようなガベージコレクタを持つ言語で書かれたコンパイラの場合、不要なオブジェクトの生成を減らすことは、ガベージコレクションの頻度と時間を削減し、結果としてコンパイル時間を短縮することにつながります。
- キャッシュ効率の向上: メモリ使用量が少ないほど、CPUのキャッシュにデータが収まりやすくなり、メモリアクセスのパフォーマンスが向上する可能性があります。
ノードの再利用は、これらの目標を達成するための一般的な最適化手法の一つです。同じ意味を持つノードが複数回必要になる場合、毎回新しいノードを生成するのではなく、一度生成したノードをプールしておき、必要に応じて再利用することで、メモリ割り当てのオーバーヘッドを削減し、メモリフットプリントを抑えることができます。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのレジスタ最適化フェーズ(regopt
)におけるONAME
ノードの生成方法の変更にあります。
ONAME
ノードがレジスタを表すためにどのように使われていたか
regopt
関数内では、特定の物理レジスタ(例: AX
, BX
など)を抽象的に表現するために、ONAME
ノードが使用されていました。regname
配列には、これらのレジスタに対応する文字列(例: ".ax"
, ".cx"
など)が格納されており、この文字列をlookup
関数でシンボルとして検索し、そのシンボルに対応する新しいONAME
ノードをnewname
関数で作成していました。
変更前のコードでは、regopt
が呼び出されるたびに、NREGVAR
(レジスタ変数の数)の回数だけループが実行され、そのループ内でnewname(lookup(regname[i]))
が呼び出されていました。これは、regopt
が複数回実行される場合(例えば、異なる関数に対して)、同じレジスタを表すONAME
ノードがその都度新しく生成されることを意味します。
なぜ再利用が必要だったのか(重複生成によるメモリ消費)
前述の通り、同じレジスタを表すONAME
ノードがregopt
の呼び出しごとに重複して生成されると、不要なメモリ割り当てが発生します。これらのノードは、レジスタ最適化フェーズの間だけ必要とされる一時的なオブジェクトですが、ガベージコレクタがそれらを回収するまでメモリを占有し続けます。
コンパイル対象のプログラムが大きくなればなるほど、regopt
が処理する関数の数も増え、結果として生成される重複ノードの数も増加し、メモリ消費量が増大します。このコミットは、この重複生成を排除し、一度生成したレジスタ用のONAME
ノードを再利用することで、メモリフットプリントを削減することを目的としています。
regnodes
配列の導入とその役割
この問題を解決するために、static Node* regnodes[NREGVAR];
という静的配列が導入されました。
static
: この配列がファイルスコープを持ち、そのファイル内のすべての関数からアクセス可能であることを意味します。また、プログラムの実行期間中、一度だけ初期化され、その値が保持されます。Node*
:Node
型へのポインタを格納する配列であることを示します。Node
はGoコンパイラ内部のASTノードの基本構造です。regnodes[NREGVAR]
:NREGVAR
は定義済みの定数で、レジスタ変数の最大数を表します。つまり、この配列は、各レジスタに対応するONAME
ノードへのポインタを格納するために使用されます。
このregnodes
配列は、各レジスタに対応するONAME
ノードを一度だけ生成し、そのポインタを保持するためのキャッシュとして機能します。
newname(lookup(regname[i]))
の呼び出しがどのように変更されたか
変更後のコードでは、regopt
内のループでvar[i].node
を初期化する際に、まずregnodes[i]
がN
(GoコンパイラにおけるNULL
または無効なノードを示す定数)であるかどうかをチェックします。
if(regnodes[i] == N)
regnodes[i] = newname(lookup(regname[i]));
var[i].node = regnodes[i];
- 初回アクセス時:
regnodes[i]
がN
の場合、これはそのレジスタに対応するONAME
ノードがまだ生成されていないことを意味します。このとき、newname(lookup(regname[i]))
が呼び出され、新しいONAME
ノードが生成され、そのポインタがregnodes[i]
に格納されます。 - 2回目以降のアクセス時:
regnodes[i]
がN
でない場合、これは以前にそのレジスタに対応するONAME
ノードが生成され、regnodes[i]
にキャッシュされていることを意味します。この場合、newname
の呼び出しはスキップされ、既存のregnodes[i]
に格納されているノードが再利用されます。
このようにして、ONAME
ノードはregopt
の呼び出し間で再利用されるようになり、重複するノードの生成が抑制され、メモリ使用量が削減されます。
コアとなるコードの変更箇所
このコミットは、src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
の3つのファイルに共通の変更を加えています。以下に、src/cmd/5g/reg.c
の変更差分を示します。他の2つのファイルも同様の変更です。
diff --git a/src/cmd/5g/reg.c b/src/cmd/5g/reg.c
index 0181ba4ba4..100cff2dee 100644
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -170,6 +170,8 @@ static char* regname[] = {\n ".F15",\n };\n \n+static Node* regnodes[NREGVAR];\n+\n void\n regopt(Prog *firstp)\n {\n@@ -216,8 +218,11 @@ regopt(Prog *firstp)\n */\n nvar = NREGVAR;\n memset(var, 0, NREGVAR*sizeof var[0]);\n-\tfor(i=0; i<NREGVAR; i++)\n-\t\tvar[i].node = newname(lookup(regname[i]));\n+\tfor(i=0; i<NREGVAR; i++) {\n+\t\tif(regnodes[i] == N)\n+\t\t\tregnodes[i] = newname(lookup(regname[i]));\n+\t\tvar[i].node = regnodes[i];\n+\t}\n \n regbits = RtoB(REGSP)|RtoB(REGLINK)|RtoB(REGPC);\n for(z=0; z<BITS; z++) {
コアとなるコードの解説
上記の差分は、Goコンパイラのレジスタ最適化ロジックにおける重要な変更を示しています。
-
static Node* regnodes[NREGVAR];
の追加:- これは、
regname
配列の定義の直後、regopt
関数の前に挿入された新しい静的グローバル変数です。 NREGVAR
は、コンパイラが扱うレジスタ変数の最大数を定義するマクロまたは定数です。- この配列は、各レジスタに対応する
ONAME
ノードへのポインタをキャッシュするために使用されます。static
キーワードにより、この配列はプログラムの実行期間中、一度だけ初期化され、その状態が保持されます。これにより、regopt
関数が複数回呼び出されても、レジスタを表すONAME
ノードが重複して生成されるのを防ぎます。
- これは、
-
for
ループ内の変更:- 変更前は、
for(i=0; i<NREGVAR; i++) var[i].node = newname(lookup(regname[i]));
という一行で、各レジスタに対応する新しいONAME
ノードが毎回生成され、var[i].node
に割り当てられていました。 - 変更後は、ループの中身がブロックになり、条件分岐が追加されています。
if(regnodes[i] == N) regnodes[i] = newname(lookup(regname[i])); var[i].node = regnodes[i];
if(regnodes[i] == N)
: これは、現在のインデックスi
に対応するレジスタのONAME
ノードが、regnodes
配列にまだキャッシュされていないかどうかをチェックします。N
は、Goコンパイラ内部でノードがNULL
または未割り当てであることを示すために使用される定数です。regnodes[i] = newname(lookup(regname[i]));
: もしregnodes[i]
がN
であれば(つまり、まだノードが生成されていない場合)、newname(lookup(regname[i]))
が呼び出され、新しいONAME
ノードが生成されます。この新しく生成されたノードのポインタは、regnodes[i]
に格納され、次回以降の再利用のためにキャッシュされます。var[i].node = regnodes[i];
: 最後に、var[i].node
には、regnodes[i]
に格納されているノード(新しく生成されたものか、キャッシュから再利用されたもの)が割り当てられます。
- 変更前は、
この変更により、regopt
が呼び出されるたびにレジスタ用のONAME
ノードが重複して生成されることがなくなり、一度生成されたノードが再利用されるため、コンパイラのメモリ使用量が削減されます。
関連リンク
- Go CL (Change List) 6902064: https://golang.org/cl/6902064
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/5g/reg.c
,src/cmd/6g/reg.c
,src/cmd/8g/reg.c
の関連部分) - Goコンパイラの内部構造に関する一般的な知識
- コンパイラ最適化、特にレジスタ割り当てに関する一般的な情報
- Go言語のIssueトラッカーやメーリングリストでの関連議論 (もしあれば)
- Go compiler source code on GitHub
- Go compiler design documents (if publicly available) (General reference for Go compiler design)
- Go language documentation
- Wikipedia: Register allocation (General concept of register allocation)
- Wikipedia: Abstract syntax tree (General concept of AST)