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

[インデックス 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コンパイラの内部で匿名戻り値パラメータを識別し、追跡するためのメカニズムを導入したことです。

  1. 匿名 PPARAMOUT の名前ノード合成:

    • src/cmd/gc/dcl.cfuncargs 関数が変更されました。この関数は、関数の引数と戻り値を処理する役割を担っています。
    • 以前は、名前のない戻り値パラメータ (n->left == N) の場合、対応する名前ノードが作成されませんでした。
    • 変更後、このような匿名パラメータに対して、.anon%d の形式(例: .anon0, .anon1)で一意のシンボルを持つ新しい名前ノードが合成されるようになりました。
    • この合成された名前ノードの orig フィールドは N (nil) に設定されます。これは、この名前ノードが元々名前を持たない匿名パラメータのために合成されたものであることを示すマーカーとして機能します。
    • これにより、コンパイラの他の部分(特にエスケープ解析)が、これらの匿名パラメータを名前付きパラメータと同様に、Node構造体を通じて参照・追跡できるようになります。
    • また、n->left->vargen = i++; という行が追加され、各戻り値パラメータに一意の生成番号が割り当てられるようになりました。これは、パラメータフロー追跡において、特定のパラメータを識別するための追加情報として利用される可能性があります。
  2. orig フィールドの利用と匿名性の保持:

    • Node構造体には、origというフィールドが存在します。これは、ノードが変換されたり、新しいノードが生成されたりした場合に、元のノードへの参照を保持するために使用されます。
    • このコミットでは、匿名戻り値パラメータのために合成された名前ノードのorigNに設定することで、その「匿名性」を明示的にマークしています。
    • src/cmd/gc/closure.cclosurehdr 関数では、クロージャの戻り値パラメータを処理する際に、元のorigフィールドの値を保持するロジックが追加されました。これにより、合成された匿名ノードがクロージャ内で再利用される場合でも、その匿名性が正しく伝播されます。
    • src/cmd/gc/dcl.cstructfield および functype 関数では、フィールドや関数の型情報を処理する際に、n->left->orig != N という条件が追加されました。これは、その名前ノードが元々名前を持っていた(つまり、合成された匿名ノードではない)場合にのみ、その名前を考慮するという意味合いを持ちます。これにより、内部的に合成された匿名ノードが、外部に公開される型情報やエラーメッセージに誤って現れることを防ぎます。
  3. 合成シンボルのエクスポート防止:

    • src/cmd/gc/fmt.csymfmt 関数に、合成されたシンボル(名前が.で始まるもの)がエクスポートされようとした場合に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コンパイラのソースコード (上記GitHub URL)
  • Go言語のエスケープ解析に関するブログ記事や解説(一般的な知識として)
  • Go言語の型システムとASTに関する資料(一般的な知識として)