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

[インデックス 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コンパイラのレジスタ最適化ロジックにおける重要な変更を示しています。

  1. static Node* regnodes[NREGVAR]; の追加:

    • これは、regname配列の定義の直後、regopt関数の前に挿入された新しい静的グローバル変数です。
    • NREGVARは、コンパイラが扱うレジスタ変数の最大数を定義するマクロまたは定数です。
    • この配列は、各レジスタに対応するONAMEノードへのポインタをキャッシュするために使用されます。staticキーワードにより、この配列はプログラムの実行期間中、一度だけ初期化され、その状態が保持されます。これにより、regopt関数が複数回呼び出されても、レジスタを表すONAMEノードが重複して生成されるのを防ぎます。
  2. 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ノードが重複して生成されることがなくなり、一度生成されたノードが再利用されるため、コンパイラのメモリ使用量が削減されます。

関連リンク

参考にした情報源リンク