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

[インデックス 19375] ファイルの概要

コミット

commit 1357f548b06c7e6b0934c565418ab7af3e6ea783
Author: Russ Cox <rsc@golang.org>
Date:   Thu May 15 19:16:18 2014 -0400

    cmd/gc: fix two select temporary bugs
    
    The introduction of temporaries in order.c was not
    quite right for two corner cases:
    
    1) The rewrite that pushed new variables on the lhs of
    a receive into the body of the case was dropping the
    declaration of the variables. If the variables escape,
    the declaration is what allocates them.
    Caught by escape analysis sanity check.
    In fact the declarations should move into the body
    always, so that we only allocate if the corresponding
    case is selected. Do that. (This is an optimization that
    was already present in Go 1.2. The new order code just
    made it stop working.)
    
    Fixes #7997.
    
    2) The optimization to turn a single-recv select into
    an ordinary receive assumed it could take the address
    of the destination; not so if the destination is _.
    
    Fixes #7998.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/100480043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1357f548b06c7e6b0934c565418ab7af3e6ea783

元コミット内容

cmd/gc: fix two select temporary bugs

The introduction of temporaries in order.c was not
quite right for two corner cases:

1) The rewrite that pushed new variables on the lhs of
a receive into the body of the case was dropping the
declaration of the variables. If the variables escape,
the declaration is what allocates them.
Caught by escape analysis sanity check.
In fact the declarations should move into the body
always, so that we only allocate if the corresponding
case is selected. Do that. (This is an optimization that
was already present in Go 1.2. The new order code just
made it stop working.)

Fixes #7997.

2) The optimization to turn a single-recv select into
an ordinary receive assumed it could take the address
of the destination; not so if the destination is _.

Fixes #7998.

LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/100480043

変更の背景

このコミットは、Goコンパイラのcmd/gc(現在のcmd/compile)におけるselectステートメントの処理に関する2つの重要なバグを修正するものです。これらのバグは、コンパイラのorder.cファイルに一時変数を導入した際に発生しました。

具体的には、以下の問題がありました。

  1. selectcase節における変数宣言の欠落: selectcase節でチャネルからの受信(<-ch)によって新しい変数を宣言する際(例: case x := <-ch:)、コンパイラがこれらの変数の宣言(ODCLノード)を適切に処理せず、case本体に移動させるべき宣言が失われていました。特に、これらの変数がエスケープ解析によってヒープに割り当てられる必要がある場合、宣言が失われると正しくメモリが割り当てられず、ランタイムエラーや不正な動作を引き起こす可能性がありました。これはGo 1.2で既に存在していた最適化(対応するcaseが選択された場合にのみ変数を割り当てる)が、新しいorderコードによって機能しなくなったために発生しました。エスケープ解析の健全性チェックによってこの問題が検出されました。

  2. 単一受信selectの最適化における_(ブランク識別子)の誤った扱い: 単一の受信操作のみを含むselectステートメント(例: select { case x := <-ch: ... })は、コンパイラによって通常の受信操作(x := <-ch)に最適化されることがあります。この最適化の際、コンパイラは受信結果の格納先のアドレスを取得できると仮定していました。しかし、受信結果が_(ブランク識別子)に割り当てられる場合(例: case _, ok := <-ch:)、_は値を保持しないためアドレスを持つことができません。この誤った仮定がコンパイルエラーや不正なコード生成を引き起こしていました。

これらのバグは、Goプログラムの正確なコンパイルと実行を妨げるものであり、特にselectステートメントの挙動に影響を与えるため、修正が急務でした。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語およびGoコンパイラに関する前提知識が必要です。

1. Go言語のselectステートメント

selectステートメントは、複数のチャネル操作を待機し、準備ができた最初のチャネル操作を実行するために使用されます。これは、非同期処理や並行処理において非常に重要な構文です。

select {
case <-ch1:
    // ch1からの受信
case val := <-ch2:
    // ch2からの受信とvalへの代入
case ch3 <- data:
    // ch3への送信
default:
    // どのチャネル操作も準備ができていない場合
}

selectcase節でval := <-chのように新しい変数を宣言し、チャネルから受信した値をその変数に代入することができます。

2. Goコンパイラ(cmd/gc / cmd/compile

Goコンパイラは、Goソースコードを機械語に変換するツールチェーンの一部です。このコミットが対象としているcmd/gcは、当時のGoコンパイラの主要な部分でした(現在はcmd/compileに名称変更されています)。

コンパイラは、ソースコードを抽象構文木(AST)にパースし、様々な最適化やコード生成のフェーズを経て最終的なバイナリを生成します。

3. order.cwalk.c

  • order.c: このファイルは、Goコンパイラの「順序付け(ordering)」フェーズを担当します。このフェーズでは、ASTノードの評価順序を決定し、必要に応じて一時変数を導入したり、複雑な式をより単純な操作に分解したりします。selectステートメントの内部的な変換もここで行われます。コミットメッセージにある「The introduction of temporaries in order.c」は、このファイルでの一時変数の導入に関する変更を指しています。

  • walk.c: このファイルは、ASTを「ウォーク(walk)」し、各ノードに対して特定の処理(型チェック、最適化、コード生成の準備など)を実行します。このコミットでは、単一受信selectの最適化に関連する部分が変更されています。

4. 一時変数(Temporaries)

コンパイラは、複雑な式の中間結果を保持するために一時変数を生成します。これらの変数は、ソースコードには明示的に現れませんが、コンパイルされたコードの正確性を保証するために内部的に使用されます。

5. エスケープ解析(Escape Analysis)

エスケープ解析は、Goコンパイラが行う最適化の一つです。変数が関数スコープを「エスケープ」して、その変数が宣言された関数がリターンした後も参照され続ける可能性があるかどうかを判断します。エスケープすると判断された変数は、スタックではなくヒープに割り当てられます。これにより、ガベージコレクタがそのメモリを管理できるようになります。

このコミットのバグ1は、エスケープ解析によってヒープ割り当てが必要と判断された変数について、その宣言が適切に処理されないために発生しました。

6. ASTノードの種類

Goコンパイラは、ASTを構成するために様々なノードタイプを使用します。このコミットで言及されている主要なノードタイプは以下の通りです。

  • ODCL (Opcode Declaration): 変数宣言を表すノードです。コンパイラが変数を割り当てるために使用します。
  • OAS (Opcode Assignment): 代入操作を表すノードです。
  • OSELRECV / OSELRECV2: selectステートメントのcase節におけるチャネル受信操作を表すノードです。OSELRECVは単一の値の受信(例: x := <-ch)、OSELRECV2は値と成功フラグの受信(例: x, ok := <-ch)に対応します。
  • OADDR: アドレス取得操作(&演算子)を表すノードです。
  • _(ブランク識別子): Go言語の特殊な識別子で、値を破棄するために使用されます。_に代入された値は使用されず、メモリも割り当てられません。

7. Node構造体と関連フィールド

GoコンパイラのASTノードは、通常Node構造体で表現されます。このコミットのコード変更で登場するNode構造体のフィールドは以下の通りです。

  • n->op: ノードの操作コード(例: ODCL, OAS)。
  • n->left, n->right: ノードの左オペランド、右オペランド。
  • n->ninit: ノードの初期化ステートメントのリスト。
  • n->colas: :=(短い変数宣言)によって宣言された変数かどうかを示すフラグ。
  • n->ntest: OSELRECV2の場合の2番目の戻り値(ok変数など)。
  • n->type: ノードの型情報。

これらの概念を理解することで、コミットで行われた具体的なコード変更の意図と影響を深く把握することができます。

技術的詳細

このコミットは、Goコンパイラのselectステートメントの処理における2つの特定の問題を解決します。これらの問題は、order.cにおける一時変数の導入と、selectの最適化ロジックの不備に起因していました。

問題1: select受信ケースにおける変数宣言の欠落(Fixes #7997

問題の発生メカニズム: Go 1.2では、selectcase節で新しい変数を宣言し、チャネルから受信する際(例: case x := <-ch)、その変数の宣言(ODCLノード)は、対応するcaseが実際に選択された場合にのみ実行されるように、case本体の内部に移動される最適化が行われていました。これにより、不要な変数割り当てを防いでいました。

しかし、order.cに一時変数を導入する新しいコードが追加された際、この最適化が正しく機能しなくなりました。具体的には、case x := <-chのような構文で宣言される変数xODCLノードが、selectステートメントの処理中に誤って削除されていました。

この問題は、特に変数xがエスケープ解析によってヒープに割り当てられる必要があると判断された場合に顕著でした。宣言が失われると、コンパイラは変数xのためのメモリを割り当てることができず、結果としてランタイムエラーや不正なメモリ参照が発生しました。issue7997.goのテストケースは、このエスケープ解析の健全性チェックによって検出された問題を示しています。

修正内容: この問題は、src/cmd/gc/order.corderstmt関数内のOSELRECVおよびOSELRECV2ケースの処理で修正されました。

  1. 既存のODCLノードの削除と再生成:
    • r->colas:=による宣言を示すフラグ)が設定されている場合、既存のODCLノード(r->ninitに格納されている可能性のある)を削除します。これは、これらの宣言がcase本体に移動されるべきであるためです。
    • r->ninitにまだ初期化ノードが残っている場合はエラーを報告します。これは、selectの受信ケースではninitが空であるべきという前提があるためです。
  2. case本体へのODCLノードの移動:
    • 受信変数(r->left)と、OSELRECV2の場合の2番目の戻り値(r->ntest、通常はok変数)について、それぞれ新しいODCLノードを生成します。
    • これらの新しいODCLノードを、selectcase本体の初期化リスト(l->n->ninit)に追加します。これにより、変数の宣言と割り当てが、そのcaseが選択された場合にのみ行われるようになります。
    • ordertemp関数を使用して、受信結果を一時変数に格納し、その一時変数を元の変数に代入するOASノードを生成します。これにより、受信操作と変数への代入が分離され、コンパイラがより柔軟に処理できるようになります。

この修正により、selectcase節で宣言された変数が、エスケープ解析の結果に関わらず、適切なタイミングで正しく宣言・割り当てされるようになりました。

問題2: 単一受信selectの最適化における_(ブランク識別子)の誤った扱い(Fixes #7998

問題の発生メカニズム: Goコンパイラは、単一の受信操作のみを含むselectステートメントを、より効率的な通常の受信操作に最適化する場合があります。例えば、select { case x := <-ch: ... }は、x := <-chに変換されることがあります。

この最適化の際、コンパイラは受信結果を格納する変数(または一時変数)のアドレスを取得しようとします。これは、チャネル受信の内部的な実装で、受信バッファから直接値を受け取るためにポインタが必要となる場合があるためです。

しかし、受信結果が_(ブランク識別子)に割り当てられる場合(例: case _, ok := <-ch:)、_は値を保持しないため、アドレスを持つことができません。コンパイラが_のアドレスを取得しようとすると、不正なコード生成やコンパイルエラーが発生していました。issue7998.goのテストケースは、この問題を示しています。

修正内容: この問題は、src/cmd/gc/walk.cwalkexpr関数内の単一受信selectの最適化部分で修正されました。

  • 変更前は、受信結果の格納先(n->list->n)に対して常にOADDRノード(アドレス取得)を生成していました。
  • 変更後は、isblank(n->list->n)関数を使用して、受信結果の格納先が_(ブランク識別子)であるかどうかをチェックします。
  • もし_であれば、nodnil()を呼び出してnilノードを生成します。これにより、_のアドレスを取得しようとする不正な操作が回避されます。
  • _でなければ、これまで通りOADDRノードを生成します。

この修正により、コンパイラは_に受信結果が割り当てられる場合でも、正しく単一受信selectの最適化を実行できるようになりました。

これらの修正は、Goコンパイラの堅牢性を高め、selectステートメントのより正確で効率的なコンパイルを保証します。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。

  1. src/cmd/gc/order.c:

    • orderstmt関数のOSELRECVおよびOSELRECV2ケースの処理が大幅に修正・追加されました。
    • 具体的には、r->colasフラグのチェックと、それに続くODCLノードの処理ロジックが追加されています。
    • r->ninitのチェックとエラー報告が追加されました。
    • 受信変数(r->left)とok変数(r->ntest)に対する新しいODCLノードの生成と、l->n->ninitへの追加ロジックが導入されました。
    • OSENDケースにもr->ninitのチェックが追加されました。

    変更行数: 36行の追加。

  2. src/cmd/gc/walk.c:

    • walkexpr関数内の単一受信selectの最適化部分が変更されました。
    • 具体的には、n1 = nod(OADDR, n->list->n, N); の行が、isblank(n->list->n)のチェックを含む条件分岐に置き換えられました。

    変更行数: 4行の追加、1行の削除。

  3. test/fixedbugs/issue7997.go:

    • 問題1(#7997)を再現し、修正を検証するための新しいテストファイルが追加されました。このテストは、selectcase節で宣言された変数がエスケープする場合の挙動を検証します。

    変更行数: 53行の追加。

  4. test/fixedbugs/issue7998.go:

    • 問題2(#7998)を再現し、修正を検証するための新しいテストファイルが追加されました。このテストは、単一受信select_(ブランク識別子)が使用される場合の挙動を検証します。

    変更行数: 23行の追加。

全体として、4つのファイルで合計116行が追加され、1行が削除されています。

コアとなるコードの解説

src/cmd/gc/order.c の変更点

このファイルでは、selectステートメントのcase節における変数宣言の処理が修正されました。

@@ -781,8 +781,30 @@ orderstmt(Node *n, Order *order)
 			fatal("order select ninit");
 		if(r != nil) {
 			switch(r->op) {
+				default:
+					yyerror("unknown op in select %O", r->op);
+					dump("select case", r);
+					break;
+
 				case OSELRECV:
 				case OSELRECV2:
+					// If this is case x := <-ch or case x, y := <-ch, the case has
+					// the ODCL nodes to declare x and y. We want to delay that
+					// declaration (and possible allocation) until inside the case body.
+					// Delete the ODCL nodes here and recreate them inside the body below.
+					if(r->colas) {
+						t = r->ninit;
+						if(t != nil && t->n->op == ODCL && t->n->left == r->left)
+							t = t->next;
+						if(t != nil && t->n->op == ODCL && t->n->left == r->ntest)
+							t = t->next;
+						if(t == nil)
+							r->ninit = nil;
+					}
+					if(r->ninit != nil) {
+						yyerror("ninit on select recv");
+						dumplist("ninit", r->ninit);
+					}
 					// case x = <-c
 					// case x, ok = <-c
 					// r->left is x, r->ntest is ok, r->right is ORECV, r->right->left is c.
@@ -803,6 +825,11 @@ orderstmt(Node *n, Order *order)
 						// such as in case interfacevalue = <-intchan.
 						// the conversion happens in the OAS instead.
 						tmp1 = r->left;
+						if(r->colas) {
+							tmp2 = nod(ODCL, tmp1, N);
+							typecheck(&tmp2, Etop);
+							l->n->ninit = list(l->n->ninit, tmp2);
+						}
 					r->left = ordertemp(r->right->left->type->type, order, haspointers(r->right->left->type->type));
 					tmp2 = nod(OAS, tmp1, r->left);
 					typecheck(&tmp2, Etop);
@@ -812,6 +839,11 @@ orderstmt(Node *n, Order *order)
 					r->ntest = N;
 					if(r->ntest != N) {
 						tmp1 = r->ntest;
+						if(r->colas) {
+							tmp2 = nod(ODCL, tmp1, N);
+							typecheck(&tmp2, Etop);
+							l->n->ninit = list(l->n->ninit, tmp2);
+						}
 					r->ntest = ordertemp(tmp1->type, order, 0);
 					tmp2 = nod(OAS, tmp1, r->ntest);
 					typecheck(&tmp2, Etop);
@@ -821,6 +853,10 @@ orderstmt(Node *n, Order *order)
 					break;
 
 				case OSEND:
+					if(r->ninit != nil) {
+						yyerror("ninit on select send");
+						dumplist("ninit", r->ninit);
+					}
 					// case c <- x
 					// r->left is c, r->right is x, both are always evaluated.
 					orderexpr(&r->left, order);
  • defaultケースの追加: switch(r->op)defaultケースが追加され、未知の操作コードがselectcaseで検出された場合にエラーを報告するようになりました。これは防御的なプログラミングであり、コンパイラの堅牢性を高めます。
  • OSELRECV / OSELRECV2 の処理強化:
    • if(r->colas)ブロック: r->colasは、:=(短い変数宣言)によって変数が宣言されたことを示します。このブロックでは、r->ninit(ノードの初期化リスト)から既存のODCLノード(変数宣言ノード)を削除しようとします。これは、これらの宣言がselectcase本体に移動されるべきであるためです。r->leftr->ntestok変数)に対応するODCLノードを探し、リストから削除します。
    • if(r->ninit != nil)ブロック: r->ninitが空でない場合、つまり、ODCLノード以外の初期化ノードがselectの受信ケースに残っている場合はエラーを報告します。これは、selectの受信ケースではninitが空であるべきというコンパイラの内部的な期待に反するためです。
    • 新しいODCLノードの生成と移動: tmp1 = r->left; の後と tmp1 = r->ntest; の後に、それぞれif(r->colas)ブロックが追加されました。
      • このブロック内で、nod(ODCL, tmp1, N)を使って新しいODCLノードを生成します。これは、tmp1で表される変数の宣言を意味します。
      • typecheck(&tmp2, Etop)で型チェックを行います。
      • l->n->ninit = list(l->n->ninit, tmp2); を使って、この新しいODCLノードをselectcase本体の初期化リスト(l->n->ninit)に追加します。これにより、変数の宣言と割り当てが、そのcaseが実際に選択された場合にのみ行われるようになります。
  • OSENDケースの処理強化: OSENDケースにもif(r->ninit != nil)ブロックが追加され、selectの送信ケースでninitが空でない場合にエラーを報告するようになりました。これは、受信ケースと同様に、コンパイラの内部的な期待に反する状況を検出するためです。

これらの変更により、selectcase節で宣言された変数が、:=による宣言であるかどうかにかかわらず、適切なタイミングで(caseが選択されたときにのみ)正しく宣言・割り当てされるようになり、エスケープ解析との整合性が保たれます。

src/cmd/gc/walk.c の変更点

このファイルでは、単一受信selectの最適化における_(ブランク識別子)の扱いが修正されました。

@@ -666,7 +666,10 @@ walkexpr(Node **np, NodeList **init)
 		r = n->rlist->n;
 		walkexprlistsafe(n->list, init);
 		walkexpr(&r->left, init);
-\t\tn1 = nod(OADDR, n->list->n, N);\n+\t\tif(isblank(n->list->n))\n+\t\t\tn1 = nodnil();\n+\t\telse\n+\t\t\tn1 = nod(OADDR, n->list->n, N);\n 		n1->etype = 1; // addr does not escape
 		fn = chanfn("chanrecv2", 2, r->left->type);
 		r = mkcall1(fn, types[TBOOL], init, typename(r->left->type), r->left, n1);
  • 変更前は、n1 = nod(OADDR, n->list->n, N); のように、受信結果の格納先(n->list->n)に対して常にアドレス取得ノード(OADDR)を生成していました。
  • 変更後は、if(isblank(n->list->n))という条件分岐が追加されました。
    • isblank(n->list->n)は、n->list->n_(ブランク識別子)であるかどうかをチェックする関数です。
    • もし_であれば、n1 = nodnil(); が実行され、nilノードが生成されます。これにより、_のアドレスを取得しようとする不正な操作が回避されます。_は値を保持しないため、アドレスを持つ必要がありません。
    • _でなければ、elseブロックのn1 = nod(OADDR, n->list->n, N); が実行され、これまで通りアドレス取得ノードが生成されます。

この変更により、単一受信selectの最適化が、受信結果が_に割り当てられる場合でも正しく機能するようになり、コンパイルエラーを防ぎます。

関連リンク

参考にした情報源リンク

  • (特になし)