[インデックス 14700] ファイルの概要
このコミットは、Goコンパイラのガベージコレクション(gc
)ツールにおけるエスケープ解析の誤ったアサーションを修正するものです。具体的には、複数の戻り値を持つ関数呼び出しの結果が単一のノードに割り当てられる際に発生していた致命的なエラー(fatal
)を解消します。この問題は、go f(g())
や defer f(g())
のような構文で g()
の戻り値がシンク(破棄される場所)にエスケープ解析される場合に自然に発生していました。
コミット
commit 1dcf658f6dbc6b09ead3fb7561cd1832d9a697a1
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Dec 20 23:27:28 2012 +0100
cmd/gc: remove an incorrect assertion in escape analysis.
A fatal error used to happen when escassign-ing a multiple
function return to a single node. However, the situation
naturally appears when using "go f(g())" or "defer f(g())",
because g() is escassign-ed to sink.
Fixes #4529.
R=golang-dev, lvd, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6920060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1dcf658f6dbc6b09ead3fb7561cd1832d9a697a1
元コミット内容
cmd/gc: remove an incorrect assertion in escape analysis.
A fatal error used to happen when escassign-ing a multiple
function return to a single node. However, the situation
naturally appears when using "go f(g())" or "defer f(g())",
because g() is escassign-ed to sink.
Fixes #4529.
R=golang-dev, lvd, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6920060
変更の背景
Goコンパイラのエスケープ解析において、複数の戻り値を持つ関数(例: g()
)の戻り値が、単一の変数やシンク(破棄される場所)に割り当てられる際に、コンパイラが誤ったアサーション(プログラムの前提条件が満たされていることを確認するチェック)を実行し、致命的なエラーでクラッシュするというバグが存在しました。
この問題は、特に go f(g())
や defer f(g())
のような構文で顕在化しました。これらのケースでは、g()
の戻り値が直接利用されず、f()
に渡されるか、あるいは単に破棄される(シンクにエスケープ解析される)ため、エスケープ解析器が「複数の戻り値が単一のノードに割り当てられている」という状況に遭遇し、それが予期せぬ状態であると判断してクラッシュしていました。
このコミットは、この誤ったアサーションを削除し、複数の戻り値が単一の宛先にフローする状況を正しく処理するようにエスケープ解析器のロジックを修正することで、コンパイラの安定性を向上させることを目的としています。
前提知識の解説
Goのエスケープ解析 (Escape Analysis)
エスケープ解析は、コンパイラ最適化の一種で、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。
- スタック割り当て: 関数内で宣言され、その関数の実行が終了するとスコープを抜ける変数は、通常スタックに割り当てられます。スタックは高速で、ガベージコレクションのオーバーヘッドがありません。
- ヒープ割り当て: 変数がその宣言されたスコープを「エスケープ」し、関数の実行が終了した後も参照され続ける可能性がある場合(例: ポインタが関数の外に返される、グローバル変数に代入される、クロージャによってキャプチャされるなど)、その変数はヒープに割り当てられます。ヒープ割り当てされたオブジェクトはガベージコレクタによって管理されます。
エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ガベージコレクションの負荷を減らし、プログラムのパフォーマンスを向上させることです。
go
ステートメントと defer
ステートメント
go
ステートメント: ゴルーチン(軽量な並行実行スレッド)を起動します。go
の後に続く関数呼び出しは、新しいゴルーチンで非同期に実行されます。defer
ステートメント:defer
の後に続く関数呼び出しは、その関数がリターンする直前に実行されるようにスケジュールされます。これはリソースの解放(ファイルクローズ、ロック解除など)によく使用されます。
これらのステートメント内で関数呼び出しが行われる場合、その関数の戻り値が直接変数に代入されないことがあります。例えば、go f(g())
の場合、g()
の戻り値は f()
の引数として渡されますが、その過程で一時的に「シンク」(破棄される場所)として扱われることがあります。
Issue 4529
GoのIssue 4529は、「escape analysis crashes on "go f(g())" when g has multiple returns.」(g
が複数の戻り値を持つ場合に、go f(g())
でエスケープ解析がクラッシュする)というタイトルで報告されたバグです。このコミットの変更は、この特定のバグを修正することを目的としています。
技術的詳細
Goコンパイラのsrc/cmd/gc/esc.c
ファイルは、エスケープ解析のロジックを実装しています。このファイル内のescassign
関数は、あるノード(src
)から別のノード(dst
)への値の割り当てをエスケープ解析の観点から処理します。
問題となっていたのは、OCALLMETH
、OCALLFUNC
、OCALLINTER
(メソッド呼び出し、関数呼び出し、インターフェースメソッド呼び出し)のケースです。以前のコードでは、これらの呼び出し元ノードの戻り値リスト(src->escretval
)の要素数が1でない場合、つまり複数の戻り値がある場合にfatal("escassign from call %+N", src)
というアサーションがトリガーされ、コンパイラがクラッシュしていました。
しかし、go f(g())
や defer f(g())
のようなシナリオでは、g()
が複数の戻り値を持つにもかかわらず、それらの戻り値が f()
の引数として渡される際に、エスケープ解析の内部では「シンク」という単一の宛先にフローしていると見なされることがあります。この状況は、コンパイラが予期しないものではなく、むしろ自然に発生するべきケースでした。
修正は、この誤ったアサーションを削除し、代わりに複数の戻り値を持つ関数呼び出しの場合でも、src->escretval
リストをイテレートし、それぞれの戻り値ノード(ll->n
)をdst
にフローさせるように変更しました。これにより、複数の戻り値が単一の宛先にフローする状況が正しく処理され、コンパイラのクラッシュが回避されます。
コアとなるコードの変更箇所
src/cmd/gc/esc.c
の escassign
関数内の OCALLMETH
, OCALLFUNC
, OCALLINTER
のケースが変更されています。
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -715,9 +716,10 @@ escassign(EscState *e, Node *dst, Node *src)
case OCALLMETH:
case OCALLFUNC:
case OCALLINTER:
- if(count(src->escretval) != 1)
- fatal("escassign from call %+N", src);
- escflows(e, dst, src->escretval->n);
+ // Flowing multiple returns to a single dst happens when
+ // analyzing "go f(g())": here g() flows to sink (issue 4529).
+ for(ll=src->escretval; ll; ll=ll->next)
+ escflows(e, dst, ll->n);
break;
また、この修正に関連して、test/escape2.go
には新しいテストケース foo82
が追加され、test/fixedbugs/issue4529.go
という新しいテストファイルが作成されています。
コアとなるコードの解説
変更前のコードでは、OCALLMETH
, OCALLFUNC
, OCALLINTER
のいずれかの種類の呼び出しノード(src
)からescassign
が呼ばれた際、src->escretval
(呼び出しの戻り値のリスト)の要素数が1でない場合にfatal
エラーを発生させていました。これは、「複数の戻り値が単一の宛先に割り当てられることはない」という前提に基づいたアサーションでした。
しかし、go f(g())
や defer f(g())
のようなケースでは、g()
が複数の戻り値を返すにもかかわらず、それらの戻り値が f()
の引数として渡される際に、エスケープ解析の内部では「シンク」(破棄される場所)という単一の抽象的な宛先にフローしていると見なされることがあります。この状況は、コンパイラがクラッシュする原因となっていました。
変更後のコードでは、この誤ったアサーションが削除され、代わりにNodeList *ll;
という新しい変数が追加されています。そして、for(ll=src->escretval; ll; ll=ll->next)
ループを使って、src->escretval
リスト内のすべての戻り値ノード(ll->n
)に対して escflows(e, dst, ll->n)
を呼び出すように修正されました。
escflows
関数は、エスケープ解析におけるデータフローを確立する役割を担っています。この変更により、複数の戻り値を持つ関数呼び出しであっても、それぞれの戻り値が適切にdst
(この場合はシンクなど)にフローするように処理されるため、コンパイラがクラッシュすることなく、エスケープ解析が続行できるようになりました。
test/fixedbugs/issue4529.go
は、この修正が正しく機能することを確認するための新しいテストケースです。このテストケースは、go b.c(c.I())
のような構造を含んでおり、c.I()
が複数の戻り値を持ち、それがgo
ステートメント内で処理されるシナリオをシミュレートしています。
関連リンク
- Go Issue 4529: https://github.com/golang/go/issues/4529
- Go CL 6920060: https://golang.org/cl/6920060
参考にした情報源リンク
- Go Issue 4529 (上記と同じ)
- Go CL 6920060 (上記と同じ)
- Goのエスケープ解析に関する一般的な情報源(例: Goの公式ドキュメント、ブログ記事など)
- A Guide to the Go Compiler: Escape Analysis: https://go.dev/doc/articles/go_compiler_guide.html#escape-analysis
- Go Escape Analysis: https://www.ardanlabs.com/blog/2017/05/go-escape-analysis.html
- Understanding Go Escape Analysis: https://medium.com/a-journey-with-go/go-understanding-escape-analysis-f709ad4d2742
- (これらのリンクは一般的な情報源であり、このコミットに直接言及しているわけではありませんが、エスケープ解析の背景知識を提供するために参照しました。)