[インデックス 14232] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc)における関数インライン化の挙動、特にローカル変数を持つ関数のインライン化に関する修正と改善を目的としています。Goコンパイラのフロントエンド、型チェック、およびインライン化処理に関連する複数のファイルが変更されています。
コミット
commit 530147e870eee380a1533f5f83ef7efe73b7139b
Author: Luuk van Dijk <lvd@golang.org>
Date: Mon Oct 29 13:55:27 2012 +0100
cmd/gc: inlining functions with local variables
- make sure dclcontext == PAUTO only in function bodies
- introduce PDISCARD to discard declarations in bodies of repeated imports
- skip printing initializing OAS'es in export mode, assuming they only occur after ODCL's
- remove ODCL and the initializing OAS from inl.c:ishairy
- fix confused use of ->typecheck in typecheckinl: it's about the ->inl, not about the fn.
- debuging aids: print ntype on ONAMEs too and -Emm instead of -Ell.
fixes #2812
R=rsc
CC=golang-dev
https://golang.org/cl/6800043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/530147e870eee380a1533f5f83ef7efe73b7139b
元コミット内容
cmd/gc: inlining functions with local variables
- make sure dclcontext == PAUTO only in function bodies
- introduce PDISCARD to discard declarations in bodies of repeated imports
- skip printing initializing OAS'es in export mode, assuming they only occur after ODCL's
- remove ODCL and the initializing OAS from inl.c:ishairy
- fix confused use of ->typecheck in typecheckinl: it's about the ->inl, not about the fn.
- debuging aids: print ntype on ONAMEs too and -Emm instead of -Ell.
fixes #2812
R=rsc
CC=golang-dev
https://golang.org/cl/6800043
変更の背景
このコミットは、Goコンパイラ(cmd/gc)における関数インライン化のバグ修正と改善を目的としています。特に、ローカル変数を持つ関数のインライン化が正しく行われない問題に対処しています。Go言語の初期バージョンでは、コンパイラの最適化機能であるインライン化はまだ成熟しておらず、特定のコードパターン(特にローカル変数の扱い)で問題が発生することがありました。
コミットメッセージに記載されている fixes #2812 は、GoのIssueトラッカーにおけるバグ報告 Issue 2812: cmd/gc: inlining functions with local variables に対応しています。このIssueでは、ローカル変数を持つ関数がインライン化される際に、変数のスコープや初期化が正しく処理されず、コンパイルエラーや不正なコード生成につながる可能性が指摘されていました。
この修正は、コンパイラの安定性と最適化の正確性を向上させるために不可欠でした。インライン化はプログラムの実行性能を向上させる重要な最適化手法であり、その正確な動作はGoプログラムの信頼性に直結します。
前提知識の解説
このコミットを理解するためには、Goコンパイラの内部構造と、コンパイラ最適化における「インライン化」の概念に関する基本的な知識が必要です。
Goコンパイラ (cmd/gc) の概要
cmd/gc は、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。その処理は、主に以下のフェーズに分けられます。
- 字句解析 (Lexing) と構文解析 (Parsing): ソースコードをトークンに分解し、抽象構文木 (AST: Abstract Syntax Tree) を構築します。
go.yファイルは、この構文解析(Yacc/Bisonによって生成されるパーサー)に関連しています。 - 型チェック (Type Checking): AST上の各ノードの型が正しいか検証し、型の推論や解決を行います。
- 中間表現 (IR: Intermediate Representation) への変換: ASTをコンパイラ内部で扱いやすい中間表現に変換します。
- 最適化 (Optimization): 中間表現に対して様々な最適化を適用し、生成されるコードの性能を向上させます。インライン化もこのフェーズの一部です。
- コード生成 (Code Generation): 最適化された中間表現から最終的な機械語コードを生成します。
インライン化 (Inlining)
インライン化は、コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換える手法です。これにより、関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減し、プログラムの実行速度を向上させることができます。
例:
func add(a, b int) int {
return a + b
}
func main() {
result := add(1, 2) // ここで add 関数がインライン化されると
}
インライン化後(概念的):
func main() {
result := 1 + 2 // add 関数の本体が直接埋め込まれる
}
しかし、インライン化にはいくつかの課題があります。
- コードサイズの増加: インライン化された関数のコードが呼び出し箇所にコピーされるため、実行ファイルのサイズが増加する可能性があります。
- コンパイル時間の増加: インライン化の判断やコードのコピーに時間がかかる場合があります。
- 複雑なケースの処理: 再帰関数、大きな関数、ローカル変数やクロージャを持つ関数など、複雑なケースではインライン化が困難になったり、特別な処理が必要になったりします。
コンパイラ内部の概念
Node: Goコンパイラ内部でASTのノードを表す構造体。各ノードは、演算子(op)、シンボル(sym)、型(type)、子ノード(left,right,list)などの情報を持つ。dclcontext: 宣言のコンテキストを示すグローバル変数。PAUTOは自動変数(ローカル変数)の宣言コンテキストを示す。OAS(OpAssign): 代入演算子を表すノード。ODCL(OpDeclare): 変数宣言を表すノード。inl: インライン化された関数のボディを表すノードリスト。typecheck: ノードが型チェック済みであるかを示すフラグ。ishairy: 関数がインライン化に適さない("hairy" = 複雑な)特性を持つかどうかを判断する関数。
技術的詳細
このコミットは、Goコンパイラの複数の側面、特にインライン化と宣言の処理に影響を与えています。
-
dclcontextの厳密化 (dcl.c):dclcontext == PAUTOが関数本体内でのみ設定されるように変更されました。これは、ローカル変数の宣言コンテキストが正しく管理されることを保証し、インライン化されたコードが誤ったコンテキストで変数を宣言するのを防ぎます。PDISCARDという新しい宣言コンテキストが導入されました。これは、重複するインポートのボディ内で宣言を破棄するために使用されます。これにより、コンパイラが同じ宣言を複数回処理するのを防ぎ、効率を向上させます。
-
エクスポートモードでの初期化
OASのスキップ (fmt.c):- コンパイラがコードをエクスポートする際(例えば、他のパッケージからインポートされるために)、初期化を伴う
OAS(代入)ノードの出力がスキップされるようになりました。これは、これらの代入が通常ODCL(宣言)の後に発生し、ODCLが再解析されて同じ代入を再生成すると仮定しているためです。これにより、エクスポートされるコードの冗長性が減り、インポート時の処理が簡素化されます。
- コンパイラがコードをエクスポートする際(例えば、他のパッケージからインポートされるために)、初期化を伴う
-
ishairy関数の改善 (inl.c):inl.c内のishairy関数は、関数がインライン化に適しているかを判断します。このコミットでは、ODCL(宣言)と初期化を伴うOAS(代入)がishairyのチェックから除外されました。これは、これらの操作がインライン化を妨げる「複雑な」要素とは見なされなくなったことを意味します。これにより、より多くの関数がインライン化の対象となる可能性があります。
-
typecheckinlのtypecheckフラグの修正 (inl.c):typecheckinl関数における->typecheckフラグの使い方が修正されました。以前は、関数自体 (fn) の型チェック状態を誤って参照していましたが、この修正により、インライン化されるボディ (->inl) の型チェック状態を正しく参照するようになりました。これは、インライン化されたコードが正しく型チェックされることを保証するために重要です。- ローカル関数に対する
typecheckinlの呼び出しが早期リターンするようになりました。typecheckinlはインポートされた関数にのみ適用されるべきであり、ローカル関数は既に型チェックされているためです。
-
デバッグ補助機能の追加 (
fmt.c,go.y):- デバッグ時に
ONAMEノード(名前を表すノード)のntype(名前の型)も出力されるようになりました。これにより、コンパイラの内部状態をより詳細に確認できるようになります。 - デバッグフラグ
-Ellが-Emmに変更されました。これは、インライン化に関するデバッグ出力の制御方法の変更を示唆しています。
- デバッグ時に
-
構文解析器の変更 (
go.y,y.tab.c):go.y(Go言語の文法定義ファイル) に変更が加えられ、重複するインポートの処理が改善されました。特に、PDISCARDコンテキストが導入され、重複する関数宣言のボディがスキップされるようになりました。これにより、パーサーが重複する定義を効率的に処理し、不必要なエラーを回避できます。y.tab.cはgo.yから自動生成されるファイルであり、go.yの変更に伴って更新されています。行番号の変更が多数見られますが、これはgo.yの変更が反映された結果です。
これらの変更は、Goコンパイラのインライン化ロジックをより堅牢にし、特にローカル変数を持つ関数のインライン化を正確に行うための基盤を強化しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その中での重要な変更箇所は以下の通りです。
-
src/cmd/gc/dcl.c:declare関数にif(ctxt == PDISCARD) return;が追加され、PDISCARDコンテキストでの宣言を破棄するようになりました。variter関数に小さな変更(空白の追加)がありますが、これは機能的な変更ではありません。
-
src/cmd/gc/fmt.c:stmtfmt関数内のcase OAS:ブロックに、エクスポートモード (fmtmode == FExp) での初期化OASの出力をスキップするロジックが追加されました。nodedump関数に、デバッグ時にONAMEノードのntypeを出力するロジックが追加されました。
-
src/cmd/gc/go.h:enumに新しい宣言コンテキストPDISCARDが追加されました。
-
src/cmd/gc/go.y:hidden_fndclルール内で、重複するインポートされた関数の型が一致する場合にdclcontext = PDISCARD;を設定し、関数ヘッダの処理をスキップするようになりました。hidden_importルール内で、関数ボディがスキップされる場合にdclcontext = PEXTERN;を設定するようになりました。- デバッグ出力のフラグが
debug['l']からdebug['m']に変更されました。
-
src/cmd/gc/inl.c:typecheckinl関数からif(fn->typecheck) return;が削除され、ローカル関数に対する早期リターンロジックが追加されました。また、fn->typecheck = 1;の設定も削除されました。caninl関数にif(fn->typecheck == 0) fatal("caninl on non-typechecked function %N", fn);が追加され、型チェックされていない関数に対するインライン化を禁止するようになりました。ishairy関数からODCLと初期化を伴うOASのチェックが削除されました。
-
src/cmd/gc/y.tab.c:go.yの変更に伴い、自動生成されたファイルであるため、多数の行番号の変更と、go.yで定義されたロジックの反映が見られます。特に、yyreduce関数内のcase 302(関数インポートの処理) でdclcontext = PEXTERN;が設定されるロジックが追加されています。
コアとなるコードの解説
src/cmd/gc/dcl.c の変更
@@ -172,6 +172,9 @@ declare(Node *n, int ctxt)
Sym *s;
int gen;
static int typegen, vargen;
+
+ if(ctxt == PDISCARD)
+ return;
declare 関数は、Goコンパイラがシンボルを宣言する際に呼び出されます。PDISCARD は新しい宣言コンテキストで、重複するインポートなどで宣言を破棄する必要がある場合に使用されます。この変更により、PDISCARD コンテキストで呼び出された declare は何もせずに即座にリターンし、不必要な宣言処理を回避します。
src/cmd/gc/fmt.c の変更
@@ -813,6 +813,12 @@ stmtfmt(Fmt *f, Node *n)
break;
case OAS:
+ // Don't export "v = <N>" initializing statements, hope they're always
+ // preceded by the DCL which will be re-parsed and typecheck to reproduce
+ // the "v = <N>" again.
+ if(fmtmode == FExp && n->right == N)
+ break;
+
if(n->colas && !complexinit)
fmtprint(f, "%N := %N", n->left, n->right);
else
stmtfmt 関数は、GoコンパイラがASTノードを文字列形式にフォーマットする際に使用されます。OAS は代入を表すノードです。この変更は、コンパイラがコードをエクスポートする際(fmtmode == FExp)、右辺が N(nil)である初期化代入文(例: var x int のようなゼロ値初期化)をスキップするようにします。これは、これらの初期化が通常、対応する ODCL(宣言)によって再生成されると仮定しているためです。これにより、エクスポートされるコードの冗長性が減ります。
src/cmd/gc/go.h の変更
@@ -649,6 +649,8 @@ enum
PPARAMREF, // param passed by reference
PFUNC,
+ PDISCARD, // discard during parse of duplicate import
+
PHEAP = 1<<7,
};
PDISCARD という新しい列挙値が dclcontext の種類として追加されました。これは、重複するインポートを解析する際に宣言を破棄するためのコンテキストとして使用されます。
src/cmd/gc/go.y の変更
@@ -1293,8 +1293,10 @@ hidden_fndcl:
importsym(s, ONAME);
if(s->def != N && s->def->op == ONAME) {
-\t\t\tif(eqtype(t, s->def->type))\n+\t\t\tif(eqtype(t, s->def->type)) {\n+\t\t\t\tdclcontext = PDISCARD; // since we skip funchdr below\n \t\t\t\tbreak;\n+\t\t\t}\n \t\t\tyyerror("inconsistent definition for func %S during import\n\t%T\n\t%T", s, s->def->type, t);
}
hidden_fndcl は、インポートされた関数の宣言を処理する文法ルールです。この変更により、もしインポートしようとしている関数の型が既に定義されている関数と一致する場合、dclcontext を PDISCARD に設定し、その後の関数ヘッダの処理をスキップするようになりました。これにより、重複する関数定義によるエラーを回避し、効率的に処理を進めます。
src/cmd/gc/inl.c の変更
@@ -82,20 +82,18 @@ typecheckinl(Node *fn)
Pkg *pkg;
int save_safemode, lno;
-\tif(fn->typecheck)\n-\t\treturn;\n-\n \tlno = setlineno(fn);\
\n-\tif (debug['m']>2)\n-\t\tprint("typecheck import [%S] %lN { %#H }\n", fn->sym, fn, fn->inl);\n-\n-\t// typecheckinl is only used for imported functions;\n+\t// typecheckinl is only for imported functions;\n \t// their bodies may refer to unsafe as long as the package\n \t// was marked safe during import (which was checked then).\n+\t// the ->inl of a local function has been typechecked before caninl copied it.\n \tpkg = fnpkg(fn);\n \tif (pkg == localpkg || pkg == nil)\n-\t\tfatal("typecheckinl on local function %lN", fn);\n+\t\treturn; // typecheckinl on local function
typecheckinl 関数は、インライン化される関数のボディの型チェックを行います。この変更により、typecheckinl がローカル関数に対して呼び出された場合(pkg == localpkg || pkg == nil)、即座にリターンするようになりました。これは、ローカル関数は既に型チェックされているため、再度型チェックする必要がないという最適化です。以前は、fn->typecheck フラグに基づいて早期リターンしていましたが、そのロジックがより明確になりました。
@@ -193,19 +192,11 @@ ishairy(Node *n, int *budget)
case OSWITCH:
case OPROC:
case ODEFER:
-\tcase ODCL:\t// declares locals as globals b/c of @"". qualification
\tcase ODCLTYPE: // can't print yet
\tcase ODCLCONST: // can't print yet
\t\treturn 1;
\n \t\tbreak;
-\tcase OAS:\n-\t\t// x = <N> zero initializing assignments aren't representible in export yet.\n-\t\t// alternatively we may just skip them in printing and hope their DCL printed\n-\t\t// as a var will regenerate it\n-\t\tif(n->right == N)\n-\t\t\treturn 1;\n-\t\tbreak;\n \t}\n \n \t(*budget)--;
ishairy 関数は、関数がインライン化に適さない("hairy" = 複雑な)特性を持つかどうかを判断します。この変更により、ODCL(宣言)と、右辺が N の OAS(初期化代入)が ishairy のチェックから除外されました。これは、これらの操作がインライン化を妨げるほど複雑ではないと判断されたため、より多くの関数がインライン化の対象となる可能性を示唆しています。
関連リンク
- Go Issue 2812: https://github.com/golang/go/issues/2812
- Go CL 6800043: https://golang.org/cl/6800043 (Gerrit Code Review)
参考にした情報源リンク
- Go言語の公式ドキュメント (Goコンパイラに関する詳細な情報は、Goのソースコードリポジトリ内のドキュメントや、Goのブログ記事、関連する学術論文などで見つけることができます。)
- Goコンパイラのソースコード (
src/cmd/gcディレクトリ) - Yacc/Bison のドキュメント (構文解析器の理解のため)
- コンパイラ最適化に関する一般的な書籍やオンラインリソース (インライン化の概念理解のため)
- Go Issue Tracker (特定のバグ報告の背景理解のため)
- Go Gerrit Code Review (変更セットの議論や詳細な変更内容の確認のため)