[インデックス 10614] ファイルの概要
このコミットは、Goコンパイラ(gc
)におけるエクスポートフォーマットの変更と、それに伴うインライン化の準備に関するものです。具体的には、コンパイラが生成するパッケージのエクスポートデータ形式が更新され、その新しい形式を正しく解釈するためにgcimporter
パッケージにも最小限の変更が加えられています。これにより、将来的な関数インライン化最適化の基盤が構築されます。
コミット
commit 40b2fe004fd35dbf5f07deaf33e0fb42a0495bf1
Author: Luuk van Dijk <lvd@golang.org>
Date: Mon Dec 5 14:40:19 2011 -0500
gc: changes in export format in preparation of inlining.
Includes minimal change to gcimporter to keep it working,
R=rsc, gri
CC=golang-dev
https://golang.org/cl/5431046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/40b2fe004fd35dbf5f07deaf33e0fb42a0495bf1
元コミット内容
Goコンパイラのエクスポートフォーマットを変更し、関数インライン化の準備を行う。これには、gcimporter
が引き続き機能するための最小限の変更が含まれる。
変更の背景
このコミットの主要な背景は、Go言語のコンパイラにおける関数インライン化 (Function Inlining) の導入準備です。関数インライン化は、コンパイラ最適化の一種であり、関数呼び出しのオーバーヘッドを削減し、プログラムの実行速度を向上させることを目的としています。
Go言語の設計思想では、シンプルさと高速なコンパイルが重視されていましたが、パフォーマンスの向上も常に重要な目標でした。関数呼び出しは、スタックフレームの作成、引数のプッシュ、戻り値の処理など、一定のオーバーヘッドを伴います。特に、小さな関数が頻繁に呼び出される場合、このオーバーヘッドが無視できないものとなります。
インライン化を実現するためには、コンパイラが呼び出し元のコードに関数本体を直接埋め込む必要があります。これには、呼び出される関数の詳細な情報(引数、戻り値、ローカル変数、関数本体のAST/IRなど)が、コンパイル時に利用可能である必要があります。Go言語では、パッケージ間の依存関係を解決し、型情報を共有するために「エクスポートフォーマット」と呼ばれる中間表現が使用されます。インライン化をサポートするためには、このエクスポートフォーマットに、関数本体のインライン化に必要な追加情報を含めるか、既存の情報の表現方法を変更する必要がありました。
このコミットは、そのための基盤となる変更、すなわちエクスポートフォーマットの更新と、それに伴うコンパイラの各部分(特にパーサー、型チェッカー、インポーター)の調整を行っています。
前提知識の解説
1. Goコンパイラ (gc
) の構造
Go言語の公式コンパイラであるgc
は、複数のフェーズに分かれて動作します。
- 字句解析 (Lexing): ソースコードをトークンに分解します。
- 構文解析 (Parsing): トークン列から抽象構文木 (AST: Abstract Syntax Tree) を構築します。
src/cmd/gc/go.y
(Yacc/Bisonの文法定義ファイル) がこのフェーズで重要な役割を果たします。 - 型チェック (Type Checking): ASTの各ノードの型を検証し、型の一貫性を保証します。
src/cmd/gc/typecheck.c
などが関連します。 - 中間表現 (IR: Intermediate Representation) 生成: ASTをコンパイラ内部で扱いやすい形式に変換します。
- 最適化 (Optimization): IRに対して様々な最適化を適用します。関数インライン化もこのフェーズの一部です。
- コード生成 (Code Generation): IRからターゲットアーキテクチャの機械語コードを生成します。
2. エクスポートフォーマット (Export Format)
Goコンパイラは、パッケージをコンパイルする際に、そのパッケージが外部に公開する情報(エクスポートされた型、関数、変数、定数など)を特殊なバイナリ形式で出力します。これが「エクスポートフォーマット」です。他のパッケージがそのパッケージをインポートする際、gcimporter
のようなツールがこのエクスポートフォーマットを読み込み、必要な型情報やシンボル情報を取得します。これは、C/C++におけるヘッダーファイルと似た役割を果たしますが、より構造化されたバイナリ形式です。
3. 関数インライン化 (Function Inlining)
関数インライン化は、コンパイラ最適化の一種です。関数呼び出しの箇所で、呼び出される関数の本体のコードを直接展開(コピー&ペースト)することで、関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、引数の渡し、戻り値の処理など)を排除します。
- 利点:
- 関数呼び出しのオーバーヘッド削減。
- インライン化されたコードに対して、さらに多くの最適化(定数伝播、デッドコード削除など)を適用できる可能性が高まる。
- 欠点:
- コードサイズが増加する可能性がある(特に大きな関数をインライン化する場合)。
- 命令キャッシュの効率に影響を与える可能性がある。
コンパイラは、通常、ヒューリスティックに基づいてインライン化を行うかどうかを決定します(例: 関数のサイズ、呼び出し頻度など)。
4. gcimporter
gcimporter
は、Goコンパイラが生成したエクスポートフォーマットを読み込み、Goプログラムが他のパッケージの型情報やシンボルを利用できるようにするためのライブラリです。エクスポートフォーマットの変更は、直接的にgcimporter
の変更を必要とします。なぜなら、新しい形式のデータを正しくパースし、内部のデータ構造にマッピングする必要があるからです。
5. Go言語のシンボルとパッケージ
Go言語では、識別子(変数名、関数名、型名など)は、それがエクスポートされるか(大文字で始まる)、パッケージ内部に留まるか(小文字で始まる)によって区別されます。エクスポートされたシンボルは他のパッケージから参照可能ですが、内部シンボルは参照できません。このコミットでは、エクスポートフォーマット内でこれらのシンボルがどのように表現され、インポート時にどのように解決されるかにも変更が加えられています。
技術的詳細
このコミットは、Goコンパイラの複数のコンポーネントにわたる広範な変更を含んでおり、その中心にはエクスポートフォーマットの更新とインライン化の準備があります。
-
エクスポートフォーマットの変更:
src/cmd/gc/fmt.c
とsrc/cmd/gc/export.c
において、シンボル(特にメソッド名)や型(特に構造体フィールド)の出力形式が変更されています。fmt.c
のsymfmt
関数では、メソッド名がエクスポートされるかどうかに応じて、パッケージ修飾子を付加するかどうかのロジックが調整されました。特に、%hhS
フォーマット指定子が「エクスポートモードではエクスポートされた場合は非修飾、そうでない場合は修飾」という新しい意味を持つようになりました。fmt.c
のtypefmt
関数では、TFIELD
(構造体フィールド)のエクスポート時の表示が変更され、匿名フィールドやインライン化された関数引数に対して?
(または_
)が明示的に出力されるようになりました。これは、インライン化されたコードのデバッグや解析を容易にするため、あるいはコンパイラ内部での表現をより正確に反映するためと考えられます。src/pkg/exp/types/gcimporter.go
のparseName
およびparseMethodDecl
関数は、新しいエクスポートフォーマットに対応するために更新されました。特に、@
プレフィックスを持つエクスポートされた名前や、?
で表される匿名名が正しくパースされるようになりました。これにより、インポートされたパッケージの型情報が、インライン化に必要な粒度で正確に再構築されます。
-
パーサー (
go.y
) の変更:src/cmd/gc/go.y
では、関数宣言(fndcl
)とメソッド宣言の構文解析ロジックが大幅に修正されました。- 最も重要な変更は、
hidden_fndcl
という新しい文法規則の導入です。これは、インポートされた関数やメソッドの宣言をパースするために使用されます。この規則は、関数本体のインライン化に必要な詳細な型情報を、インポート時に取得できるように設計されています。 hidden_import
規則も更新され、インポートされた関数がimportlist
に追加されるようになりました。これは、インライン化可能な関数をコンパイラが追跡するためのメカニズムです。sym
規則に、インポート時に非エクスポート識別子をbuiltinpkg
から解決するロジックが追加されました。また、?
が匿名シンボルとして認識されるようになりました。
-
宣言と型処理 (
dcl.c
,typecheck.c
) の変更:src/cmd/gc/dcl.c
では、funchdr
関数が拡張され、funcargs2
という新しい関数が導入されました。funcargs2
は、インポート時に既に構築されたTFUNC
型(関数型)から引数を宣言するために使用されます。これは、インライン化のために必要な、より詳細な関数シグネチャの処理を可能にします。importconst
,importvar
,importtype
などのimport*
関数群がsrc/cmd/gc/export.c
で変更され、一部のexportname
やmypackage
チェックが削除されました。これは、インポート時のシンボル解決ロジックが簡素化または再設計されたことを示唆しています。特に、importmethod
関数が削除され、メソッドのインポートがgo.y
のhidden_fndcl
を通じてより統合的に処理されるようになったことを示しています。src/cmd/gc/typecheck.c
では、ODOT
(セレクタ)の型チェックロジックが改善され、特にメソッドの解決において、エクスポートされた名前と非エクスポートの名前の扱いがより厳密になりました。また、複合リテラル(typecheckcomplit
)におけるシンボル解決も、exportname
チェックを追加することで修正されています。
-
init
関数のリネーム (init.c
):src/cmd/gc/init.c
のrenameinit
関数のシグネチャが変更され、init
関数のユニークな名前生成ロジックが簡素化されました。これは、init
関数もインライン化の対象となる可能性を考慮しているか、あるいはコンパイラ内部でのinit
関数の扱いをより一貫させるための変更と考えられます。
これらの変更は、Goコンパイラがパッケージ境界を越えて関数本体の情報をより詳細に理解し、インライン化という最適化を適用するための基盤を築くものです。エクスポートフォーマットの変更は、コンパイラの異なるバージョン間での互換性に影響を与える可能性があるため、慎重に行われる必要があります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特に以下のファイルと関数が重要です。
-
src/cmd/gc/go.y
:fndcl
(関数宣言) およびメソッド宣言の文法規則の変更。hidden_fndcl
規則の新規導入: インポートされた関数の宣言をパースするための新しい規則。これがインライン化のための関数シグネチャと本体情報の取得に不可欠です。hidden_import
規則の変更:LFUNC
(関数インポート) の処理でhidden_fndcl
を使用し、インポートされた関数をimportlist
に追加するロジック。
-
src/cmd/gc/dcl.c
:funchdr
関数の変更: 関数ヘッダーの処理をより柔軟にし、特にインポートされた関数に対応。funcargs2
関数の新規導入: 既に構築されたTFUNC
型(インポートされた関数など)から引数を宣言するための関数。
-
src/cmd/gc/export.c
:dumpexporttype
の変更: メソッドシンボルのエクスポート時のフォーマット指定子の調整。importconst
,importvar
,importtype
関数の変更: インポート時のシンボル解決ロジックの調整。importmethod
関数の削除: メソッドのインポート処理がgo.y
のhidden_fndcl
に統合されたことを示唆。
-
src/cmd/gc/fmt.c
:symfmt
関数の変更: シンボル(特にメソッド名)のフォーマットロジックの調整。typefmt
関数の変更:TFIELD
(構造体フィールド)のエクスポート時の表示形式の変更(?
や_
の明示的な出力)。exprfmt
関数の変更:OSTRUCTLIT
(構造体リテラル)のエクスポート時のフィールド名フォーマットの調整。
-
src/pkg/exp/types/gcimporter.go
:parseName
関数の変更:@
プレフィックス付きのエクスポート名や?
匿名名のパースに対応。parseMethodDecl
関数の変更: メソッド名のパースにparseName
を使用するよう変更。
これらの変更は相互に関連しており、Goコンパイラのフロントエンドとバックエンド間のインターフェースであるエクスポートフォーマットの根本的な変更と、それに伴うパーサーおよびインポーターの適応を示しています。
コアとなるコードの解説
src/cmd/gc/go.y
における hidden_fndcl
の導入
--- a/src/cmd/gc/go.y
+++ b/src/cmd/gc/go.y
@@ -1234,13 +1249,51 @@ fndcl:
\t\tif(rcvr->right->op == OTPAREN || (rcvr->right->op == OIND && rcvr->right->left->op == OTPAREN))\
\t\t\tyyerror("cannot parenthesize receiver type");
-\t\t$$ = nod(ODCLFUNC, N, N);\
-\t\t$$->nname = methodname1(name, rcvr->right);\
+\t\t$$ = N;\
\t\tt = nod(OTFUNC, rcvr, N);\
\t\tt->list = $6;\
\t\tt->rlist = $8;\
+\n+\t\t$$ = nod(ODCLFUNC, N, N);\
+\t\t$$->shortname = newname($4);\
+\t\t$$->nname = methodname1($$->shortname, rcvr->right);\
+\t\t$$->nname->defn = $$;\
\t\t$$->nname->ntype = t;\
-\t\t$$->shortname = name;\
+\t\tdeclare($$->nname, PFUNC);\
+\n+\t\tfunchdr($$);\
+\t}\n+\n+hidden_fndcl:\n+\thidden_pkg_importsym '(' ohidden_funarg_list ')' ohidden_funres\n+\t{\n+\t\tSym *s;\n+\t\tType *t;\n+\n+\t\t$$ = N;\n+\n+\t\ts = $1;\n+\t\tt = functype(N, $3, $5);\n+\n+\t\timportsym(s, ONAME);\
+\t\tif(s->def != N && s->def->op == ONAME) {\
+\t\t\tif(eqtype(t, s->def->type))\
+\t\t\t\tbreak;\
+\t\t\tyyerror("inconsistent definition for func %S during import\\n\\t%T\\n\\t%T", s, s->def->type, t);\
+\t\t}\n+\n+\t\t$$ = newname(s);\
+\t\t$$->type = t;\
+\t\tdeclare($$, PFUNC);\
+\n+\t\tfunchdr($$);\
+\t}\n+|\t'(' hidden_funarg_list ')' sym '(' ohidden_funarg_list ')' ohidden_funres\n+\t{\n+\t\t$$ = methodname1(newname($4), $2->n->right); \n+\t\t$$->type = functype($2->n, $6, $8);\n+\n+\t\tcheckwidth($$->type);\
+\t\taddmethod($4, $$->type, 0);\
\tfunchdr($$);\
}\n \n```
この変更は、Goコンパイラのパーサーの核心部分に触れています。`hidden_fndcl`という新しい文法規則が導入されたことで、コンパイラはインポートされたパッケージから関数やメソッドの宣言を、より詳細な形式でパースできるようになりました。
* **`hidden_pkg_importsym '(' ohidden_funarg_list ')' ohidden_funres`**: これは、通常の関数(レシーバを持たない関数)がインポートされる際の規則です。
* `$1` (`hidden_pkg_importsym`) は関数のシンボル(名前)を表します。
* `$3` (`ohidden_funarg_list`) は関数の引数リストを表します。
* `$5` (`ohidden_funres`) は関数の戻り値リストを表します。
* この規則では、`functype`を使って関数型を構築し、`importsym`を使ってそのシンボルをインポート済みとして登録します。これにより、インポートされた関数の型情報がコンパイラ内部で利用可能になります。
* **`'(' hidden_funarg_list ')' sym '(' ohidden_funarg_list ')' ohidden_funres`**: これは、メソッド(レシーバを持つ関数)がインポートされる際の規則です。
* `$2->n->right` はレシーバの型を表します。
* `$4` (`sym`) はメソッド名を表します。
* `$6` と `$8` はそれぞれ引数と戻り値のリストです。
* `methodname1`を使ってメソッドの完全な名前を構築し、`functype`で関数型を構築します。
* `addmethod`を呼び出すことで、このメソッドがレシーバの型に関連付けられます。
これらの`hidden_fndcl`規則は、インライン化のために、インポートされた関数やメソッドのシグネチャだけでなく、その内部構造(引数や戻り値の具体的な型、名前など)をより正確に把握できるようにするためのものです。これにより、コンパイラはインライン化の判断や、インライン化後のコード生成に必要な情報を得ることができます。
### `src/cmd/gc/dcl.c` における `funcargs2` の導入
```diff
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -627,6 +624,48 @@ funcargs(Node *nt)\
\t}\n }\n \n+/*\n+ * Same as funcargs, except run over an already constructed TFUNC.\n+ * This happens during import, where the hidden_fndcl rule has\n+ * used functype directly to parse the function\'s type.\n+ */\n+static void\n+funcargs2(Type *t)\n+{\n+\tType *ft;\n+\tNode *n;\n+\n+\tif(t->etype != TFUNC)\n+\t\tfatal("funcargs2 %T", t);\n+\t\n+\tif(t->thistuple)\n+\t\tfor(ft=getthisx(t)->type; ft; ft=ft->down) {\n+\t\t\tif(!ft->nname || !ft->nname->sym)\n+\t\t\t\tcontinue;\n+\t\t\tn = newname(ft->nname->sym);\n+\t\t\tn->type = ft->type;\n+\t\t\tdeclare(n, PPARAM);\n+\t\t}\n+\n+\tif(t->intuple)\n+\t\tfor(ft=getinargx(t)->type; ft; ft=ft->down) {\n+\t\t\tif(!ft->nname || !ft->nname->sym)\n+\t\t\t\tcontinue;\n+\t\t\tn = newname(ft->nname->sym);\n+\t\t\tn->type = ft->type;\n+\t\t\tdeclare(n, PPARAM);\n+\t\t}\n+\n+\tif(t->outtuple)\n+\t\tfor(ft=getoutargx(t)->type; ft; ft=ft->down) {\n+\t\t\tif(!ft->nname || !ft->nname->sym)\n+\t\t\t\tcontinue;\n+\t\t\tn = newname(ft->nname->sym);\n+\t\t\tn->type = ft->type;\n+\t\t\tdeclare(n, PPARAMOUT);\n+\t\t}\n+}\n+\n /*\n * finish the body.\n * called in auto-declaration context.\n```
`funcargs`関数は、ソースコードからパースされたASTノード(`Node *nt`)に基づいて関数引数を宣言するのに対し、新しく導入された`funcargs2`関数は、既に構築された`Type *t`(特に`TFUNC`型)に基づいて引数を宣言します。
この違いは、インポート処理において重要です。Goコンパイラが他のパッケージをインポートする際、そのパッケージのエクスポートフォーマットから関数型情報を読み込みます。この情報は、ASTノードとしてではなく、既に内部の`Type`構造体として表現されています。`hidden_fndcl`規則が`functype`を使って直接`Type`構造体を構築するように変更されたため、その後の処理でこの`Type`構造体から引数を宣言する必要が生じました。
`funcargs2`は、`TFUNC`型の`thistuple`(レシーバ)、`intuple`(入力引数)、`outtuple`(出力引数)をイテレートし、それぞれの引数に対応するシンボル(`nname->sym`)と型(`type`)を取得して、`declare`関数で宣言します。これにより、インポートされた関数の引数も、コンパイラ内部で正しくシンボルとして認識され、型チェックやその後の最適化(インライン化など)の対象となります。
## 関連リンク
* Go言語のコンパイラ最適化に関する議論: [https://golang.org/doc/go1.1](https://golang.org/doc/go1.1) (Go 1.1リリースノートにはインライン化に関する言及があるかもしれません)
* Go言語のコンパイラソースコード: [https://github.com/golang/go/tree/master/src/cmd/compile](https://github.com/golang/go/tree/master/src/cmd/compile) (現在のコンパイラは`cmd/compile`にあります)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Goコンパイラのソースコード (特に`src/cmd/gc`ディレクトリ)
* Go言語のコンパイラに関する技術ブログや論文 (一般的なコンパイラ最適化としてのインライン化の概念)
* コミットメッセージに記載されているGoのコードレビューシステム (Gerrit) のリンク: [https://golang.org/cl/5431046](https://golang.org/cl/5431046) (このリンクは古い可能性があり、現在のGerritのURL形式とは異なる場合がありますが、当時のコードレビューの詳細が含まれている可能性があります。)
* Yacc/Bisonのドキュメント (構文解析の理解のため)
* Go言語の内部構造に関する非公式な解説記事