[インデックス 10780] ファイルの概要
このコミットは、Goコンパイラ(gc)にクロスパッケージおよびイントラパッケージのインライン化機能を追加するものです。具体的には、単一の代入文やreturn <expression>形式の関数呼び出しのインライン化をサポートします。ただし、クロージャや可変長引数(...引数)を含む式、その他の関数呼び出しなど、一部の複雑なケースは現時点では対象外とされています。この機能は、コンパイル時に-lフラグが指定されていない場合には無効になります。
コミット
commit a62722bba4e7ccfe2ebfae4a5702c17e0a64937d
Author: Luuk van Dijk <lvd@golang.org>
Date: Wed Dec 14 15:05:33 2011 +0100
gc: inlining (disabled without -l)
Cross- and intra package inlining of single assignments or return <expression>.
Minus some hairy cases, currently including other calls, expressions with closures and ... arguments.
R=rsc, rogpeppe, adg, gri
CC=golang-dev
https://golang.org/cl/5400043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a62722bba4e7ccfe2ebfae4a5702c17e0a64937d
元コミット内容
このコミットは、Goコンパイラgcにインライン化機能(関数本体を呼び出し箇所に直接展開する最適化)を導入します。この機能は、単一の代入文またはreturn <expression>形式の関数に対して、パッケージ内外を問わず適用されます。ただし、他の関数呼び出し、クロージャを含む式、可変長引数(...)を伴う引数など、一部の複雑なケースは初期段階ではサポートされていません。このインライン化は、コンパイル時に-lフラグが指定されていない場合は無効になります。
変更の背景
Go言語の初期段階において、コンパイラの最適化はまだ発展途上でした。関数呼び出しのオーバーヘッドは、特に小さな関数が頻繁に呼び出される場合にパフォーマンスのボトルネックとなる可能性があります。インライン化は、このオーバーヘッドを削減し、さらにコンパイラがより広範なコンテキストで最適化(例えば、定数伝播やデッドコード削除)を実行できるようにするための重要な最適化手法です。
このコミットは、Goコンパイラに基本的なインライン化能力を導入することで、生成されるバイナリの実行速度を向上させることを目的としています。特に、Go言語の設計思想である「シンプルさ」と「効率性」を両立させる上で、コンパイラによる自動的な最適化は不可欠でした。
コミットメッセージにある「disabled without -l」という記述は、当時のGoコンパイラの挙動を示唆しています。現代のGoコンパイラでは-lフラグはインライン化を無効にするために使われますが、このコミットが作成された2011年時点では、-lフラグがインライン化を有効にするためのデバッグフラグとして機能していた可能性が高いです。これは、新機能の導入初期段階で、開発者がその挙動を制御しやすくするための一般的なアプローチです。
前提知識の解説
Goコンパイラ gc
Go言語の公式コンパイラはgcと呼ばれます。この名前は「Go Compiler」の略であり、Goのガベージコレクション(GC)とは異なります。gcは、Goのソースコードを機械語に変換する役割を担い、複数のフェーズを経てコンパイルを行います。
コンパイラの最適化:インライン化 (Inlining)
インライン化は、コンパイラが行う最適化の一種です。関数が呼び出される際に、その関数の本体を呼び出し元のコードに直接挿入(展開)することで、関数呼び出しに伴うオーバーヘッド(スタックフレームのセットアップ、引数の渡し、戻り値の処理など)を排除します。
インライン化の利点:
- パフォーマンス向上: 関数呼び出しのオーバーヘッドがなくなるため、実行速度が向上します。
- さらなる最適化の機会: 関数本体が呼び出し元に展開されることで、コンパイラはより大きなコードブロックを分析できるようになり、定数伝播、デッドコード削除、レジスタ割り当ての最適化など、他の最適化をより効果的に適用できるようになります。
インライン化の欠点:
- バイナリサイズの増加: 同じ関数が複数回インライン化されると、その関数のコードがバイナリ内に複数コピーされるため、最終的な実行ファイルのサイズが増加する可能性があります。
- コンパイル時間の増加: インライン化の判断やコードの展開には、コンパイラに追加の処理時間が必要になります。
Goコンパイラのフェーズ
Goコンパイラgcは、一般的に以下の主要なフェーズを経てコンパイルを行います。このコミットは、特に「中間表現の構築と中間最適化」のフェーズに新しいインライン化のステップを追加しています。
- 構文解析 (Parsing): ソースコードを字句解析し、抽象構文木 (AST) を構築します。
- 型チェック (Type Checking): ASTに対して型チェックを行い、Go言語の型システムに準拠しているか検証します。
- 中間表現の構築と中間最適化 (IR Construction & Middle-end Optimizations): ASTを中間表現 (IR) に変換し、インライン化、エスケープ解析などの最適化を適用します。
- 機械語生成 (Backend): 最適化されたIRをターゲットアーキテクチャの機械語に変換します。
-l コンパイラフラグ
Goコンパイラの-lフラグは、歴史的にその挙動が変化しています。このコミットが作成された2011年時点では、-lフラグはインライン化を有効にするためのデバッグフラグとして機能していたと考えられます。コミットメッセージの「disabled without -l」は、この新しいインライン化機能が、-lフラグが指定された場合にのみ有効になることを意味しています。
現代のGoコンパイラでは、go build -gcflags="-l"のように-lフラグを指定すると、インライン化が無効になります。これは、デバッグ時や、インライン化が原因で問題が発生している場合に、スタックトレースを読みやすくするためなどに使用されます。この歴史的な違いを理解することが重要です。
技術的詳細
このコミットは、Goコンパイラのsrc/cmd/gcディレクトリ内の複数のファイルを変更し、インライン化のロジックを導入しています。
インライン化のプロセス
Goコンパイラにおけるインライン化は、大きく分けて2つのパスで構成されます。
-
caninl(Can Inline):- 各関数がインライン化に適しているかどうかを判断します。
- 現在の実装では、関数本体が単一のステートメント(
returnまたは代入)である場合にインライン化の候補とします。 - クロージャ、可変長引数、その他の関数呼び出しを含む複雑なケースは「hairy cases」としてインライン化の対象外とされます。
- インライン化が可能な関数については、その関数本体のコピー(ASTノードのリスト)を
fn->inlフィールドに保存します。
-
inlcalls(Inline Calls):- 各関数の本体を走査し、インライン化可能な関数への呼び出しを見つけます。
- 見つかった呼び出し箇所を、
OINLCALLという新しい中間表現ノードに変換します。 OINLCALLノードは、インライン化される関数の本体(fn->inlからコピーされ、引数や戻り値が適切に置換されたもの)を含みます。- インライン化された関数内のローカル変数やパラメータは、呼び出し元の関数内で新しい一時変数に置き換えられます。
return文は、インライン化された関数の戻り値を呼び出し元の戻り値変数に代入し、インライン化されたコードブロックの末尾へのgoto文に変換されます。
主要なデータ構造の変更
Node構造体(src/cmd/gc/go.h内):NodeList* inl;:インライン化可能な関数の本体(ASTノードのリスト)を格納するために追加されました。Node* inlvar;:インライン化中に元の変数を置き換えるための一時変数を指すために追加されました。
Op列挙型(src/cmd/gc/go.h内):OINLCALL:インライン化された関数呼び出しを表す新しい中間表現のオペレーションが追加されました。
コンパイルパイプラインへの組み込み
src/cmd/gc/lex.cのmain関数が変更され、インライン化のフェーズがコンパイルプロセスに組み込まれました。
- Phase 4: Inlining:
- インポートされた関数の本体の型チェックが行われます。
caninl関数が呼び出され、インライン化可能な関数が特定され、その本体がクローンされます。inlcalls関数が呼び出され、すべての関数内でインライン化可能な呼び出しが展開されます。
- 既存のフェーズの番号が変更され、インライン化がエスケープ解析の前に実行されるようになりました。
クロスパッケージインライン化のサポート
src/cmd/gc/export.cが変更され、インライン化された関数本体がパッケージのエクスポート情報に含まれるようになりました。これにより、他のパッケージからインポートされた関数もインライン化の対象となることが可能になります。具体的には、func %#S%#hT { %#H }のような形式で、関数のシグネチャに加えてインライン化された本体がエクスポートされるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/cmd/gc/inl.c(新規ファイル):- このファイルは、インライン化の主要なロジックを実装しています。
caninl関数:関数のインライン化可能性を判断し、インライン化可能な関数のASTをコピーして保存します。ishairy関数:インライン化できない複雑なケース(例:OCALL,OCLOSUREなど)を検出します。inlcopy,inlcopylist関数:ASTノードを再帰的にコピーします。inlcalls関数:関数内の呼び出しを走査し、インライン化可能な呼び出しをOINLCALLノードに変換します。mkinlcall関数:実際のインライン化処理を行い、引数の割り当て、戻り値の処理、関数本体の置換を行います。inlsubst,inlsubstlist関数:インライン化された関数本体内の変数参照を、呼び出し元の新しい一時変数に置換します。return文をgoto文と戻り値の代入に変換します。
-
src/cmd/gc/export.c:dumpexportvarおよびdumpexporttype関数が変更され、インライン化された関数本体(n->inl)をエクスポート情報に含めるようになりました。これにより、クロスパッケージインライン化が可能になります。reexportdepおよびreexportdeplist関数が追加され、インライン化された本体が必要とする外部シンボルを再エクスポートするロジックが導入されました。
-
src/cmd/gc/go.h:Node構造体にinl(インライン化された本体のコピー)とinlvar(インライン化中の変数置換用)フィールドが追加されました。OINLCALLという新しいオペレーションコードが追加され、インライン化された呼び出しを表すために使用されます。
-
src/cmd/gc/lex.c:main関数内のコンパイルフェーズの順序が変更され、インライン化フェーズが追加されました。debug['l']フラグ(当時のインライン化有効化フラグ)に基づいてインライン化処理が実行されるようになりました。
-
src/cmd/gc/go.y:- 構文解析器の定義ファイルが変更され、メソッド定義(
hidden_fndcl)やインポートされた関数(hidden_import)のASTノードに、インライン化された本体への参照($$->type->nname = $$;や$2->inl = $3;)が設定されるようになりました。
- 構文解析器の定義ファイルが変更され、メソッド定義(
コアとなるコードの解説
src/cmd/gc/inl.c の caninl 関数
void
caninl(Node *fn)
{
// ... (省略) ...
// exactly 1 statement
if(fn->nbody == nil || fn->nbody->next != nil)
return;
// the single statement should be a return or an assignment.
switch(fn->nbody->n->op) {
default:
return;
case ORETURN:
case OAS:
case OAS2:
// case OEMPTY: // TODO
break;
}
// can't handle ... args yet
for(t=fn->type->type->down->down->type; t; t=t->down)
if(t->isddd)
return;
// TODO Anything non-trivial
if(ishairy(fn))
return;
// ... (省略) ...
fn->nname->inl = fn->nbody; // インライン化可能な本体を保存
fn->nbody = inlcopylist(fn->nname->inl); // 元の本体をコピーで置き換え
// ... (省略) ...
}
caninl関数は、特定の関数fnがインライン化可能かどうかを判断します。
- 関数本体が単一のステートメントであること(
fn->nbody == nil || fn->nbody->next != nil)。 - そのステートメントが
RETURN、AS(代入)、AS2(多重代入)のいずれかであること。 - 可変長引数(
...)を使用していないこと。 ishairy関数で定義される「複雑な」ケース(例:go、defer、call、closureなど)を含まないこと。
これらの条件を満たす場合、関数の元の本体(fn->nbody)をfn->nname->inlに保存し、fn->nbody自体は保存した本体のコピーで置き換えられます。これにより、元の関数本体はインライン化のために「クリーンな」状態に保たれます。
src/cmd/gc/inl.c の mkinlcall 関数
static void
mkinlcall(Node **np, Node *fn)
{
// ... (省略) ...
if (fn->inl == nil) // インライン化可能な本体がなければ何もしない
return;
// ... (省略) ...
// パラメータの一時変数を作成
for(ll = dcl; ll; ll=ll->next)
if(ll->n->op == ONAME && ll->n->class != PPARAMOUT) {
ll->n->inlvar = inlvar(ll->n);
ninit = list(ninit, nod(ODCL, ll->n->inlvar, N));
}
// 引数をパラメータの一時変数に代入
// ... (省略) ...
// 戻り値用の一時変数を作成
inlretvars = nil;
i = 0;
for(t = getoutargx(fn->type)->type; t; t = t->down)
inlretvars = list(inlretvars, retvar(t, i++));
inlretlabel = newlabel(); // 戻り値のためのラベル
body = inlsubstlist(fn->inl); // インライン化された本体を置換
body = list(body, nod(OGOTO, inlretlabel, N)); // returnの代わりにgoto
body = list(body, nod(OLABEL, inlretlabel, N)); // returnのターゲットラベル
// ... (省略) ...
call = nod(OINLCALL, N, N); // OINLCALLノードを作成
call->ninit = ninit; // 引数代入
call->nbody = body; // インライン化された本体
call->rlist = inlretvars; // 戻り値変数
// ... (省略) ...
*np = call; // 元の呼び出しノードをOINLCALLノードで置き換え
// ... (省略) ...
}
mkinlcall関数は、実際のインライン化処理を実行します。
fn->inl(caninlで保存されたインライン化可能な関数本体)が存在しない場合は処理をスキップします。- インライン化される関数のパラメータやローカル変数に対応する新しい一時変数(
inlvar)を呼び出し元の関数内に作成し、それらの宣言をninitリストに追加します。 - 呼び出し元の引数を、これらの新しい一時変数に代入する処理を生成し、これも
ninitリストに追加します。 - インライン化される関数の戻り値に対応する一時変数(
retvar)を作成し、inlretvarsリストに保存します。 inlsubstlist(fn->inl)を呼び出して、インライン化される関数本体内の変数参照を新しい一時変数に置換し、return文をgoto文と戻り値の代入に変換します。- 最終的に、元の関数呼び出しノード(
*np)を、OINLCALLという新しいタイプのノードで置き換えます。このOINLCALLノードは、引数の代入、インライン化された関数本体、および戻り値変数を含みます。
src/cmd/gc/inl.c の inlsubst 関数
static Node*
inlsubst(Node *n)
{
// ... (省略) ...
switch(n->op) {
case ONAME:
if(n->inlvar) { // inlvarが設定されていれば置換
return n->inlvar;
}
return n; // 設定されていなければそのまま
case ORETURN:
if (closuredepth > 0) // ネストされたクロージャ内のreturnは処理しない
break;
m = nod(OGOTO, inlretlabel, N); // returnをgotoに変換
m->ninit = inlsubstlist(n->ninit);
if(inlretvars && n->list) { // 戻り値があれば代入
as = nod(OAS2, N, N);
as->list = inlretvars;
as->rlist = inlsubstlist(n->list);
typecheck(&as, Etop);
m->ninit = list(m->ninit, as);
}
return m;
}
// ... (省略) ...
// 再帰的に子ノードを処理
m->left = inlsubst(n->left);
m->right = inlsubst(n->right);
m->list = inlsubstlist(n->list);
// ... (省略) ...
}
inlsubst関数は、インライン化される関数本体のASTを再帰的に走査し、以下の置換を行います。
ONAMEノード(変数参照)の場合、もしその変数に対応するinlvar(mkinlcallで作成された一時変数)が設定されていれば、その一時変数に置き換えます。ORETURNノードの場合、これをOGOTOノードに変換し、inlretlabel(インライン化されたコードブロックの末尾のラベル)へジャンプするようにします。もし戻り値がある場合、その戻り値をinlretvars(mkinlcallで作成された戻り値用の一時変数)に代入するOAS2(多重代入)ノードを生成し、gotoの前に挿入します。これにより、インライン化された関数は、あたかも呼び出し元の関数内で直接実行されたかのように振る舞います。
これらの変更により、Goコンパイラは、特定の条件を満たす関数に対して自動的にインライン化を適用し、生成されるコードのパフォーマンスを向上させることができるようになりました。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
参考にした情報源リンク
- Go compiler phases: https://go.dev/blog/go1.7-ssa (Go 1.7でのSSA導入に関する記事だが、コンパイラのフェーズについても触れている)
- Go inlining: https://go.dev/doc/go1.9#mid-stack-inlining (Go 1.9でのミッドスタックインライン化に関する情報だが、一般的なインライン化の概念も含まれる)
- Go compiler flags: https://pkg.go.dev/cmd/go#hdr-Build_flags (Goビルドコマンドのフラグに関する公式ドキュメント)
- Stack Overflow: "What does -gcflags=-l mean in Go?" (Goの
-gcflags=-lの意味に関する議論) - Medium: "Understanding Go Compiler Optimizations" (Goコンパイラの最適化に関する記事)
- Cheney, Dave. "Go's inliner." Dave Cheney. (Goのインライナーに関するブログ記事)
- The Go Programming Language Specification: https://go.dev/ref/spec