[インデックス 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
ファイルに一時変数を導入した際に発生しました。
具体的には、以下の問題がありました。
-
select
のcase
節における変数宣言の欠落:select
のcase
節でチャネルからの受信(<-ch
)によって新しい変数を宣言する際(例:case x := <-ch:
)、コンパイラがこれらの変数の宣言(ODCL
ノード)を適切に処理せず、case
本体に移動させるべき宣言が失われていました。特に、これらの変数がエスケープ解析によってヒープに割り当てられる必要がある場合、宣言が失われると正しくメモリが割り当てられず、ランタイムエラーや不正な動作を引き起こす可能性がありました。これはGo 1.2で既に存在していた最適化(対応するcase
が選択された場合にのみ変数を割り当てる)が、新しいorder
コードによって機能しなくなったために発生しました。エスケープ解析の健全性チェックによってこの問題が検出されました。 -
単一受信
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:
// どのチャネル操作も準備ができていない場合
}
select
のcase
節でval := <-ch
のように新しい変数を宣言し、チャネルから受信した値をその変数に代入することができます。
2. Goコンパイラ(cmd/gc
/ cmd/compile
)
Goコンパイラは、Goソースコードを機械語に変換するツールチェーンの一部です。このコミットが対象としているcmd/gc
は、当時のGoコンパイラの主要な部分でした(現在はcmd/compile
に名称変更されています)。
コンパイラは、ソースコードを抽象構文木(AST)にパースし、様々な最適化やコード生成のフェーズを経て最終的なバイナリを生成します。
3. order.c
とwalk.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では、select
のcase
節で新しい変数を宣言し、チャネルから受信する際(例: case x := <-ch
)、その変数の宣言(ODCL
ノード)は、対応するcase
が実際に選択された場合にのみ実行されるように、case
本体の内部に移動される最適化が行われていました。これにより、不要な変数割り当てを防いでいました。
しかし、order.c
に一時変数を導入する新しいコードが追加された際、この最適化が正しく機能しなくなりました。具体的には、case x := <-ch
のような構文で宣言される変数x
のODCL
ノードが、select
ステートメントの処理中に誤って削除されていました。
この問題は、特に変数x
がエスケープ解析によってヒープに割り当てられる必要があると判断された場合に顕著でした。宣言が失われると、コンパイラは変数x
のためのメモリを割り当てることができず、結果としてランタイムエラーや不正なメモリ参照が発生しました。issue7997.go
のテストケースは、このエスケープ解析の健全性チェックによって検出された問題を示しています。
修正内容:
この問題は、src/cmd/gc/order.c
のorderstmt
関数内のOSELRECV
およびOSELRECV2
ケースの処理で修正されました。
- 既存の
ODCL
ノードの削除と再生成:r->colas
(:=
による宣言を示すフラグ)が設定されている場合、既存のODCL
ノード(r->ninit
に格納されている可能性のある)を削除します。これは、これらの宣言がcase
本体に移動されるべきであるためです。r->ninit
にまだ初期化ノードが残っている場合はエラーを報告します。これは、select
の受信ケースではninit
が空であるべきという前提があるためです。
case
本体へのODCL
ノードの移動:- 受信変数(
r->left
)と、OSELRECV2
の場合の2番目の戻り値(r->ntest
、通常はok
変数)について、それぞれ新しいODCL
ノードを生成します。 - これらの新しい
ODCL
ノードを、select
のcase
本体の初期化リスト(l->n->ninit
)に追加します。これにより、変数の宣言と割り当てが、そのcase
が選択された場合にのみ行われるようになります。 ordertemp
関数を使用して、受信結果を一時変数に格納し、その一時変数を元の変数に代入するOAS
ノードを生成します。これにより、受信操作と変数への代入が分離され、コンパイラがより柔軟に処理できるようになります。
- 受信変数(
この修正により、select
のcase
節で宣言された変数が、エスケープ解析の結果に関わらず、適切なタイミングで正しく宣言・割り当てされるようになりました。
問題2: 単一受信select
の最適化における_
(ブランク識別子)の誤った扱い(Fixes #7998
)
問題の発生メカニズム:
Goコンパイラは、単一の受信操作のみを含むselect
ステートメントを、より効率的な通常の受信操作に最適化する場合があります。例えば、select { case x := <-ch: ... }
は、x := <-ch
に変換されることがあります。
この最適化の際、コンパイラは受信結果を格納する変数(または一時変数)のアドレスを取得しようとします。これは、チャネル受信の内部的な実装で、受信バッファから直接値を受け取るためにポインタが必要となる場合があるためです。
しかし、受信結果が_
(ブランク識別子)に割り当てられる場合(例: case _, ok := <-ch:
)、_
は値を保持しないため、アドレスを持つことができません。コンパイラが_
のアドレスを取得しようとすると、不正なコード生成やコンパイルエラーが発生していました。issue7998.go
のテストケースは、この問題を示しています。
修正内容:
この問題は、src/cmd/gc/walk.c
のwalkexpr
関数内の単一受信select
の最適化部分で修正されました。
- 変更前は、受信結果の格納先(
n->list->n
)に対して常にOADDR
ノード(アドレス取得)を生成していました。 - 変更後は、
isblank(n->list->n)
関数を使用して、受信結果の格納先が_
(ブランク識別子)であるかどうかをチェックします。 - もし
_
であれば、nodnil()
を呼び出してnil
ノードを生成します。これにより、_
のアドレスを取得しようとする不正な操作が回避されます。 _
でなければ、これまで通りOADDR
ノードを生成します。
この修正により、コンパイラは_
に受信結果が割り当てられる場合でも、正しく単一受信select
の最適化を実行できるようになりました。
これらの修正は、Goコンパイラの堅牢性を高め、select
ステートメントのより正確で効率的なコンパイルを保証します。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。
-
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行の追加。
-
src/cmd/gc/walk.c
:walkexpr
関数内の単一受信select
の最適化部分が変更されました。- 具体的には、
n1 = nod(OADDR, n->list->n, N);
の行が、isblank(n->list->n)
のチェックを含む条件分岐に置き換えられました。
変更行数: 4行の追加、1行の削除。
-
test/fixedbugs/issue7997.go
:- 問題1(
#7997
)を再現し、修正を検証するための新しいテストファイルが追加されました。このテストは、select
のcase
節で宣言された変数がエスケープする場合の挙動を検証します。
変更行数: 53行の追加。
- 問題1(
-
test/fixedbugs/issue7998.go
:- 問題2(
#7998
)を再現し、修正を検証するための新しいテストファイルが追加されました。このテストは、単一受信select
で_
(ブランク識別子)が使用される場合の挙動を検証します。
変更行数: 23行の追加。
- 問題2(
全体として、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
ケースが追加され、未知の操作コードがselect
のcase
で検出された場合にエラーを報告するようになりました。これは防御的なプログラミングであり、コンパイラの堅牢性を高めます。OSELRECV
/OSELRECV2
の処理強化:if(r->colas)
ブロック:r->colas
は、:=
(短い変数宣言)によって変数が宣言されたことを示します。このブロックでは、r->ninit
(ノードの初期化リスト)から既存のODCL
ノード(変数宣言ノード)を削除しようとします。これは、これらの宣言がselect
のcase
本体に移動されるべきであるためです。r->left
とr->ntest
(ok
変数)に対応する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
ノードをselect
のcase
本体の初期化リスト(l->n->ninit
)に追加します。これにより、変数の宣言と割り当てが、そのcase
が実際に選択された場合にのみ行われるようになります。
- このブロック内で、
OSEND
ケースの処理強化:OSEND
ケースにもif(r->ninit != nil)
ブロックが追加され、select
の送信ケースでninit
が空でない場合にエラーを報告するようになりました。これは、受信ケースと同様に、コンパイラの内部的な期待に反する状況を検出するためです。
これらの変更により、select
のcase
節で宣言された変数が、:=
による宣言であるかどうかにかかわらず、適切なタイミングで(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
の最適化が、受信結果が_
に割り当てられる場合でも正しく機能するようになり、コンパイルエラーを防ぎます。
関連リンク
- Go CL 100480043: https://golang.org/cl/100480043
- Go Issue #7997: https://github.com/golang/go/issues/7997
- Go Issue #7998: https://github.com/golang/go/issues/7998
参考にした情報源リンク
- (特になし)