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

[インデックス 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)への値の割り当てをエスケープ解析の観点から処理します。

問題となっていたのは、OCALLMETHOCALLFUNCOCALLINTER(メソッド呼び出し、関数呼び出し、インターフェースメソッド呼び出し)のケースです。以前のコードでは、これらの呼び出し元ノードの戻り値リスト(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.cescassign 関数内の 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ステートメント内で処理されるシナリオをシミュレートしています。

関連リンク

参考にした情報源リンク