[インデックス 14205] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるパラメータフロー追跡機能の初期ステップとして、匿名(名前なし)の戻り値パラメータ(PPARAMOUT
)に対して、内部的に名前ノードを生成する変更を導入しています。これは、エスケープ解析の精度向上を目的とした、より広範なパラメータフロー追跡機能の実装に向けた基盤作りとなります。既存のコンパイラの動作に影響を与えないよう、慎重に実装されています。
コミット
commit 976ca1a47d91a6d07fcae2abcbb59e8300c3adea
Author: Luuk van Dijk <lvd@golang.org>
Date: Mon Oct 22 10:09:52 2012 +0200
cmd/gc: track parameter flow, step 0: synthesize name nodes for anonymous PPARAMOUTs without breaking anything.
further work on parameter flow tracking for escape analysis depends on this.
R=rsc
CC=golang-dev
https://golang.org/cl/6600044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/976ca1a47d91a6d07fcae2abcbb59e8300c3adea
元コミット内容
cmd/gc: track parameter flow, step 0: synthesize name nodes for anonymous PPARAMOUTs without breaking anything.
further work on parameter flow tracking for escape analysis depends on this.
R=rsc
CC=golang-dev
https://golang.org/cl/6600044
変更の背景
Goコンパイラは、プログラムの効率を最適化するために様々な解析を行います。その一つが「エスケープ解析(Escape Analysis)」です。エスケープ解析は、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定する重要なプロセスです。スタック割り当ては高速ですが、関数呼び出しの終了とともに変数が破棄されます。一方、ヒープ割り当てはより柔軟ですが、ガベージコレクションのオーバーヘッドが発生します。
エスケープ解析の精度を高めるためには、関数内外でのデータの流れ、特にパラメータや戻り値がどのように利用されるかを正確に追跡する「パラメータフロー追跡」が不可欠です。Go言語では、関数が複数の戻り値を返すことができ、それらの戻り値に明示的な名前を付けない「匿名戻り値パラメータ」を定義することが可能です(例: func() (int, error)
)。
このコミットの背景には、エスケープ解析を改善するために、これらの匿名戻り値パラメータも内部的に追跡できるようにする必要がありました。しかし、匿名であるため、コンパイラの内部表現ではそれらを識別するための「名前ノード」が存在しませんでした。このコミットは、その問題を解決し、将来のパラメータフロー追跡およびエスケープ解析の改善のための「ステップ0」として、匿名戻り値パラメータに内部的な名前ノードを合成することを目的としています。これにより、コンパイラがこれらの匿名パラメータを他の名前付きパラメータと同様に扱えるようになり、より詳細なデータフロー解析が可能になります。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラであり、ソースコードを機械語に変換する役割を担います。cmd/gc
は、フロントエンド(構文解析、型チェック)からバックエンド(コード生成)までの一連の処理を行います。 - 抽象構文木 (AST): ソースコードの構造を木構造で表現したものです。コンパイラはASTを操作して、プログラムの意味を理解し、最適化を行います。Goコンパイラでは、ASTの各ノードが
Node
構造体で表現されます。 - シンボル (
Sym
): 変数、関数、型などの識別子に対応するコンパイラ内部のデータ構造です。シンボルは、その識別子の名前、型、スコープなどの情報を含みます。 - パラメータ (
PPARAM
,PPARAMOUT
):PPARAM
: 関数の入力パラメータ(引数)を表します。PPARAMOUT
: 関数の出力パラメータ(戻り値)を表します。Goでは、戻り値にも名前を付けることができますが、名前を付けない匿名戻り値も可能です。
- 名前ノード (
ONAME
): ASTにおいて、変数や関数などの名前付きエンティティを表すノードです。 - エスケープ解析 (Escape Analysis): コンパイラ最適化の一種で、変数がその宣言されたスコープを「エスケープ」して、関数呼び出し後も生存し続ける必要があるかどうかを判断します。
- スタック割り当て: 変数が関数内で完結し、関数終了時に破棄される場合、メモリはスタックに割り当てられます。これは高速で、ガベージコレクションの対象外です。
- ヒープ割り当て: 変数がスコープをエスケープし、関数終了後も参照され続ける可能性がある場合、メモリはヒープに割り当てられます。これはガベージコレクションの対象となり、オーバーヘッドが発生する可能性があります。
- エスケープ解析は、不要なヒープ割り当てを減らし、プログラムのパフォーマンスを向上させるために重要です。
- データフロー解析 (Data Flow Analysis): プログラム実行中にデータがどのように伝播するかを分析する技術です。エスケープ解析もデータフロー解析の一種です。
技術的詳細
このコミットの主要な技術的変更点は、Goコンパイラの内部で匿名戻り値パラメータを識別し、追跡するためのメカニズムを導入したことです。
-
匿名
PPARAMOUT
の名前ノード合成:src/cmd/gc/dcl.c
のfuncargs
関数が変更されました。この関数は、関数の引数と戻り値を処理する役割を担っています。- 以前は、名前のない戻り値パラメータ (
n->left == N
) の場合、対応する名前ノードが作成されませんでした。 - 変更後、このような匿名パラメータに対して、
.anon%d
の形式(例:.anon0
,.anon1
)で一意のシンボルを持つ新しい名前ノードが合成されるようになりました。 - この合成された名前ノードの
orig
フィールドはN
(nil) に設定されます。これは、この名前ノードが元々名前を持たない匿名パラメータのために合成されたものであることを示すマーカーとして機能します。 - これにより、コンパイラの他の部分(特にエスケープ解析)が、これらの匿名パラメータを名前付きパラメータと同様に、
Node
構造体を通じて参照・追跡できるようになります。 - また、
n->left->vargen = i++;
という行が追加され、各戻り値パラメータに一意の生成番号が割り当てられるようになりました。これは、パラメータフロー追跡において、特定のパラメータを識別するための追加情報として利用される可能性があります。
-
orig
フィールドの利用と匿名性の保持:Node
構造体には、orig
というフィールドが存在します。これは、ノードが変換されたり、新しいノードが生成されたりした場合に、元のノードへの参照を保持するために使用されます。- このコミットでは、匿名戻り値パラメータのために合成された名前ノードの
orig
をN
に設定することで、その「匿名性」を明示的にマークしています。 src/cmd/gc/closure.c
のclosurehdr
関数では、クロージャの戻り値パラメータを処理する際に、元のorig
フィールドの値を保持するロジックが追加されました。これにより、合成された匿名ノードがクロージャ内で再利用される場合でも、その匿名性が正しく伝播されます。src/cmd/gc/dcl.c
のstructfield
およびfunctype
関数では、フィールドや関数の型情報を処理する際に、n->left->orig != N
という条件が追加されました。これは、その名前ノードが元々名前を持っていた(つまり、合成された匿名ノードではない)場合にのみ、その名前を考慮するという意味合いを持ちます。これにより、内部的に合成された匿名ノードが、外部に公開される型情報やエラーメッセージに誤って現れることを防ぎます。
-
合成シンボルのエクスポート防止:
src/cmd/gc/fmt.c
のsymfmt
関数に、合成されたシンボル(名前が.
で始まるもの)がエクスポートされようとした場合にfatal
エラーを発生させるチェックが追加されました。これは、.anon%d
のような内部的なシンボルが、Goのパッケージエクスポートメカニズムを通じて外部に漏洩するのを防ぐための安全策です。
これらの変更により、Goコンパイラは匿名戻り値パラメータをより詳細に内部で管理できるようになり、将来のエスケープ解析やその他の最適化の基盤が強化されました。
コアとなるコードの変更箇所
src/cmd/gc/closure.c
--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -13,7 +13,7 @@
void
closurehdr(Node *ntype)
{
- Node *n, *name, *a;\n+\tNode *n, *name, *a, *orig;
NodeList *l;
n = nod(OCLOSURE, N, N);
@@ -43,8 +43,11 @@ closurehdr(Node *ntype)
}
for(l=n->rlist; l; l=l->next) {
name = l->n->left;
-\t\tif(name)\n+\t\tif(name) {\n+\t\t\torig = name->orig; // preserve the meaning of orig == N (anonymous PPARAMOUT)
\t\t\tname = newname(name->sym);\n+\t\t\tname->orig = orig;
+\t\t}\
ntype->rlist = list(ntype->rlist, nod(ODCLFIELD, name, l->n->right));
}
}
src/cmd/gc/dcl.c
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -596,25 +596,38 @@ funcargs(Node *nt)\n \t}\n \n \t// declare the out arguments.\n-\tgen = 0;\n+\tgen = count(nt->list);\n+\tint i = 0;\
\tfor(l=nt->rlist; l; l=l->next) {\n \t\tn = l->n;\n+\n \t\tif(n->op != ODCLFIELD)\n \t\t\tfatal(\"funcargs out %O\", n->op);\
-\t\tif(n->left != N) {\n-\t\t\tn->left->op = ONAME;\n-\t\t\tn->left->ntype = n->right;\n-\t\t\tif(isblank(n->left)) {\n-\t\t\t\t// Give it a name so we can assign to it during return.\n-\t\t\t\t// preserve the original in ->orig\n-\t\t\t\tnn = nod(OXXX, N, N);\n-\t\t\t\t*nn = *n->left;\n-\t\t\t\tn->left = nn;\n-\t\t\t\tsnprint(namebuf, sizeof(namebuf), \".anon%d\", gen++);\n-\t\t\t\tn->left->sym = lookup(namebuf);\n-\t\t\t}\n-\t\t\tdeclare(n->left, PPARAMOUT);\
+\n+\t\tif(n->left == N) {\n+\t\t\t// give it a name so escape analysis has nodes to work with
+\t\t\tsnprint(namebuf, sizeof(namebuf), \".anon%d\", gen++);\
+\t\t\tn->left = newname(lookup(namebuf));\
+\t\t\tn->left->orig = N; // signal that the original was absent
+\n+\t\t} \
+\n+\t\tn->left->op = ONAME;\
+\n+\t\tif(isblank(n->left)) {\n+\t\t\t// Give it a name so we can assign to it during return.\n+\t\t\t// preserve the original in ->orig
+\t\t\tnn = nod(OXXX, N, N);\
+\t\t\t*nn = *n->left;\
+\t\t\tn->left = nn;\
+\t\t\t\n+\t\t\tsnprint(namebuf, sizeof(namebuf), \".anon%d\", gen++);\
+\t\t\tn->left->sym = lookup(namebuf);\
\t\t}\
+\n+\t\tn->left->ntype = n->right;\
+\t\tdeclare(n->left, PPARAMOUT);\
+\t\tn->left->vargen = i++;
\t}\n }\
\n@@ -769,7 +782,7 @@ structfield(Node *n)\
\t\tbreak;\
\t}\
\n-\tif(n->left && n->left->op == ONAME) {\
+\tif(n->left && n->left->op == ONAME && n->left->orig != N) {\
\t\tf->nname = n->left;\
\t\tf->embedded = n->embedded;\
\t\tf->sym = f->nname->sym;\
@@ -1145,7 +1158,7 @@ functype(Node *this, NodeList *in, NodeList *out)\
\t\tt->thistuple = 1;\
\tt->outtuple = count(out);\
\tt->intuple = count(in);\
-\tt->outnamed = t->outtuple > 0 && out->n->left != N;\
+\tt->outnamed = t->outtuple > 0 && out->n->left != N && out->n->left->orig != N;\
\n \treturn t;\
}\
src/cmd/gc/fmt.c
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -518,6 +518,8 @@ symfmt(Fmt *fp, Sym *s)\
\t\t\t\treturn fmtprint(fp, \"%s.%s\", s->pkg->name, s->name);\t// dcommontype, typehash
\t\t\treturn fmtprint(fp, \"%s.%s\", s->pkg->prefix, s->name);\t// (methodsym), typesym, weaksym
\t\tcase FExp:\
+\t\t\tif(s->name && s->name[0] == \'.\')\
+\t\t\t\tfatal(\"exporting synthetic symbol %s\", s->name);\
\t\t\tif(s->pkg != builtinpkg)\
\t\t\t\treturn fmtprint(fp, \"@\\\"%Z\\\".%s\", s->pkg->path, s->name);\
\t\t}\
@@ -713,9 +715,13 @@ typefmt(Fmt *fp, Type *t)\
\tcase TFIELD:\
\t\tif(!(fp->flags&FmtShort)) {\
\t\t\ts = t->sym;\
+\n \t\t\t// Take the name from the original, lest we substituted it with .anon%d
-\t\t\tif (t->nname && (fmtmode == FErr || fmtmode == FExp))\
-\t\t\t\ts = t->nname->orig->sym;\
+\t\t\tif ((fmtmode == FErr || fmtmode == FExp) && t->nname != N)\
+\t\t\t\tif(t->nname->orig != N)\
+\t\t\t\t\ts = t->nname->orig->sym;\
+\t\t\t\telse \
+\t\t\t\t\ts = S;\
\t\t\t\
\t\t\tif(s != S && !t->embedded) {\
\t\t\t\tif(fp->flags&FmtLong)\
コアとなるコードの解説
src/cmd/gc/closure.c
の変更
closurehdr
関数は、クロージャのヘッダ(型情報)を構築する際に呼び出されます。- 追加された
orig
変数は、name->orig
の値を一時的に保持するために使用されます。 if(name)
ブロック内で、name = newname(name->sym);
の前にorig = name->orig;
が追加され、新しい名前ノードを作成した後でname->orig = orig;
と元のorig
の値を復元しています。- これは、匿名戻り値パラメータのために合成された名前ノード(
orig == N
)がクロージャ内で処理される際に、その匿名性を示すorig == N
という状態が失われないようにするためです。これにより、クロージャのコンテキストでも、元の匿名性が正しく伝播されます。
src/cmd/gc/dcl.c
の変更
funcargs
関数は、関数の引数と戻り値を宣言する部分です。gen = count(nt->list);
とint i = 0;
が追加され、戻り値パラメータの数をgen
に初期設定し、各パラメータに一意の番号を割り当てるためのカウンタi
が導入されました。- 最も重要な変更は、
if(n->left == N)
ブロックです。n->left == N
は、戻り値パラメータに明示的な名前が付けられていない(匿名である)ことを意味します。- この場合、
snprint(namebuf, sizeof(namebuf), ".anon%d", gen++);
で.anon0
,.anon1
のような形式の新しい名前を生成し、n->left = newname(lookup(namebuf));
でその名前を持つ新しい名前ノードを合成します。 - そして、
n->left->orig = N;
と設定することで、このノードが元々名前を持たない匿名パラメータのために合成されたものであることを明示的にマークします。
isblank(n->left)
のチェックは、Goのブランク識別子(_
)を処理するためのものです。ブランク識別子も内部的には名前ノードが必要ですが、外部には公開されません。この変更により、ブランク識別子も同様に内部的な名前(.anon%d
)が与えられ、その元の情報がorig
に保存されるようになります。n->left->vargen = i++;
は、各戻り値パラメータに一意の生成番号を割り当て、パラメータフロー追跡で利用できるようにします。structfield
関数とfunctype
関数では、n->left->orig != N
という条件が追加されました。これは、構造体フィールドや関数の型情報を処理する際に、名前ノードが元々名前を持っていた場合にのみ、その名前を考慮するという意味です。これにより、内部的に合成された匿名ノードが、外部に公開される型情報やエラーメッセージに誤って現れることを防ぎ、コンパイラの出力の整合性を保ちます。
src/cmd/gc/fmt.c
の変更
symfmt
関数は、シンボルをフォーマットする際に呼び出されます。case FExp:
ブロック内にif(s->name && s->name[0] == '.') fatal("exporting synthetic symbol %s", s->name);
が追加されました。これは、.
で始まる(つまり内部的に合成された)シンボルがエクスポートされようとした場合に、コンパイラを異常終了させることで、内部シンボルの外部漏洩を防ぐための安全チェックです。typefmt
関数は、型をフォーマットする際に呼び出されます。t->nname->orig
を参照するロジックが変更されました。特に(fmtmode == FErr || fmtmode == FExp)
の条件下で、t->nname->orig != N
であれば元のシンボルを使用し、そうでなければS
(nilシンボル) を使用するように分岐が追加されました。これは、エラーメッセージやエクスポートされる型情報において、合成された匿名パラメータの内部名が表示されないようにするためのものです。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
- Goのエスケープ解析に関する一般的な情報: https://go.dev/doc/effective_go#allocation_efficiency
参考にした情報源リンク
- Goコンパイラのソースコード (上記GitHub URL)
- Go言語のエスケープ解析に関するブログ記事や解説(一般的な知識として)
- Go言語の型システムとASTに関する資料(一般的な知識として)