[インデックス 14734] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
パッケージにおけるselect
ステートメントの評価順序に関するバグ修正を扱っています。具体的には、select
文内で変数のロードが行われる際に、その変数の値が後続の関数呼び出しによって変更される可能性がある場合でも、コンパイラがその変数の値を保存する(一時変数に退避させる)必要がないと誤って判断していた問題を修正します。これにより、select
文の実行中に予期せぬ変数の値の変更が発生する可能性がありました。
コミット
commit 1b3244e0dbbe547a0703d0380708f480a8f3c228
Author: Russ Cox <rsc@golang.org>
Date: Sat Dec 22 16:46:01 2012 -0500
cmd/gc: fix eval order in select
Ordinary variable load was assumed to be not worth saving,
but not if one of the function calls later might change
its value.
Fixes #4313.
R=ken2
CC=golang-dev
https://golang.org/cl/6997047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1b3244e0dbbe547a0703d0380708f480a8f3c228
元コミット内容
cmd/gc: fix eval order in select
通常の変数ロードは、保存する価値がないと見なされていましたが、後続の関数呼び出しのいずれかがその値を変更する可能性がある場合はそうではありませんでした。
Issue #4313 を修正します。
変更の背景
Go言語のselect
ステートメントは、複数の通信操作(チャネルの送受信)を待機し、準備ができた最初の操作を実行するための強力な制御構造です。このステートメントのセマンティクスは、各ケースの式が評価される順序と、その評価がチャネル操作の選択にどのように影響するかに依存します。
このコミットの背景には、select
ステートメント内で使用される変数の評価順序に関する潜在的なバグがありました。Goコンパイラ(cmd/gc
)は、コードを最適化する際に、一時的な変数のロードが「保存する価値がない」(つまり、その値がすぐに必要とされ、後で変更される可能性がない)と判断することがあります。しかし、select
ケース内でチャネル操作の引数として変数が使用され、かつそのチャネル操作の評価中に別の関数呼び出し(例えば、select
の他のケースの評価の一部として実行される関数)がその変数の値を変更する可能性がある場合、この最適化は誤った動作を引き起こす可能性がありました。
具体的には、select
文の評価中に、あるチャネル操作の引数として使用される変数の値が、別のチャネル操作の準備のために実行される関数によって変更されてしまうと、最初のチャネル操作が古い(変更前の)値で実行されてしまうという問題です。これは、Goのメモリモデルと評価順序の保証に反する動作であり、予測不能なプログラムの挙動につながります。
Issue #4313 はこの問題点を指摘しており、このコミットはその修正を目的としています。テストケース test/fixedbugs/issue4313.go
は、このバグがどのように顕在化するかを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。
-
Go言語の
select
ステートメント:select
ステートメントは、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行します。もし複数の操作が同時に準備できた場合、select
はランダムに1つを選択します。default
ケースが存在する場合、どのチャネル操作も準備できていない場合にdefault
ケースが実行されます。select
の各ケース内の式(チャネル式や送信値など)は、select
がどのケースを選択するかを決定する前に評価されます。この評価順序が重要です。 -
Goコンパイラ (
cmd/gc
):cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを中間表現に変換し、最終的に実行可能なバイナリを生成します。コンパイルプロセスには、構文解析、型チェック、最適化、コード生成などが含まれます。 -
評価順序 (Evaluation Order): Go言語の仕様では、式の評価順序が厳密に定義されています。特に、関数呼び出しの引数や複合リテラルの要素など、特定の操作における評価順序は、プログラムのセマンティクスを保証するために重要です。
select
ステートメントのケース内の式も、特定の順序で評価される必要があります。 -
Node
構造体とninit
リスト: Goコンパイラの内部では、プログラムの抽象構文木 (AST) がNode
構造体で表現されます。各Node
は、式、ステートメント、宣言などを表します。ninit
は、Node
構造体の一部であり、そのNode
の評価に必要な初期化ステートメントのリスト(NodeList
)を保持します。例えば、一時変数の宣言や、式の評価に伴う副作用のある操作などがninit
リストに追加されます。コンパイラは、ninit
リスト内のステートメントを、関連するNode
が評価される前に実行するようにスケジュールします。これは、式の評価順序を制御し、副作用が正しく発生するようにするために非常に重要です。 -
safeexpr
とlocalexpr
: コンパイラの最適化フェーズで使用される関数です。safeexpr(Node *n, NodeList **init)
: 式n
が安全に評価できることを保証します。もしn
が副作用を持つか、複数回評価されると問題が生じる可能性がある場合、n
の値を一時変数に退避させるなどの処理を行い、その初期化ステートメントをinit
リストに追加します。localexpr(Node *n, Type *t, NodeList **init)
: 式n
の値を、指定された型t
のローカル変数に格納します。これは、n
が複雑な式である場合や、その値が後で変更される可能性がある場合に、その値を「固定」するために使用されます。この関数は、n
が既に適切なローカル変数である場合は何もしません。
-
ONAME
ノードとaddrtaken
フラグ:ONAME
ノードは、変数や関数名などの識別子を表します。n->addrtaken
フラグは、その変数のアドレスが取得されたことがあるかどうかを示します。アドレスが取得された変数は、ポインタを介して間接的に変更される可能性があるため、コンパイラはより慎重に扱います。
技術的詳細
このコミットの技術的な核心は、select
ステートメントのケース内でチャネル操作の引数として使用される変数の評価と、その値の「固定」に関するコンパイラの挙動の修正です。
Goコンパイラは、select
ステートメントを処理する際に、各case
を内部的に変換します。この変換プロセスでは、チャネル操作(送受信)がselectsend
やselectrecv
といったランタイム関数呼び出しに置き換えられます。これらのランタイム関数は、チャネル操作の準備状況をチェックし、実際に操作を実行します。
問題は、これらのランタイム関数に渡される引数(特に送信値や受信バッファ)が、select
文の評価中に他のcase
の評価によって変更される可能性がある場合に発生しました。コンパイラは、通常の変数ロードを「保存する価値がない」と判断し、一時変数への退避を行わないことがありました。しかし、select
の文脈では、あるcase
の評価が別のcase
の評価に影響を与える可能性があるため、この仮定は危険でした。
このコミットでは、以下の主要な変更が行われています。
-
src/cmd/gc/order.c
の変更:orderstmt
関数は、ステートメントの評価順序を決定し、必要な初期化ステートメントをninit
リストに追加する役割を担います。OSELRECV2
(select receive with two return values, i.e.,v, ok := <-ch
) とOSEND
(channel send) のケースで、orderexpr
関数の第2引数がout
(現在のステートメントリスト)からl->n->ninit
(現在のselect
ケースの初期化リスト)に変更されています。これは、これらのチャネル操作の引数(受信バッファや送信値)の評価に伴う初期化ステートメントが、select
ケースの初期化リストに確実に追加されるようにするためです。これにより、これらの引数がselect
の他の部分の評価によって影響を受ける前に、その値が「固定」されるようになります。
-
src/cmd/gc/select.c
の変更:walkselect
関数は、select
ステートメントをウォークし、ランタイム関数呼び出しに変換する主要な場所です。r->nbody = cas->ninit;
がr->ninit = cas->ninit;
に変更されています。これは、select
ケースの初期化リストが、生成されるif
ステートメントのninit
リストに正しく引き継がれるようにするためです。selectdefault
,selectsend
,selectrecv
,selectrecv2
のランタイム関数呼び出し (mkcall
またはmkcall1
) の第3引数(初期化リスト)が&init
から&r->ninit
に変更されています。これは、これらのランタイム関数呼び出しの引数(チャネル、送信値、受信バッファなど)の評価に伴う初期化ステートメントが、select
ケース全体の初期化リストに確実に追加されるようにするためです。OSEND
のケースで、n->left = safeexpr(n->left, &r->ninit);
の行がn->left = localexpr(safeexpr(n->left, &r->ninit), n->left->type, &r->ninit);
に変更されています。これは、チャネル送信操作のチャネル式 (n->left
) が、localexpr
によってローカル変数に「固定」されるようにするためです。これにより、チャネル式が評価された後、その値が他のselect
ケースの評価によって変更されることを防ぎます。
-
src/cmd/gc/subr.c
の変更:localexpr
関数は、式をローカル変数に変換する役割を担います。if(n->op == ONAME &&
の条件に!n->addrtaken &&
が追加されています。これは、ONAME
ノード(変数)が既にローカル変数であり、かつそのアドレスが取得されていない場合にのみ、localexpr
がその変数をそのまま返すようにするためです。もし変数のアドレスが取得されている場合(addrtaken
がtrue)、その変数はポインタを介して間接的に変更される可能性があるため、localexpr
は新しい一時変数を作成して値をコピーする必要があります。この変更により、localexpr
は、アドレスが取得された変数に対しても、その値を安全に「固定」できるようになります。
これらの変更により、select
ステートメント内のチャネル操作の引数として使用される変数の値が、select
の評価中に他の部分の副作用によって予期せず変更されることがなくなります。特に、localexpr
の改善と、ninit
リストへの初期化ステートメントの適切な追加が、この問題の解決に貢献しています。
コアとなるコードの変更箇所
src/cmd/gc/order.c
--- a/src/cmd/gc/order.c
+++ b/src/cmd/gc/order.c
@@ -276,11 +276,11 @@ orderstmt(Node *n, NodeList **out)
case OSELRECV2:
orderexprinplace(&r->left);
orderexprinplace(&r->ntest);
- orderexpr(&r->right->left, out);
+ orderexpr(&r->right->left, &l->n->ninit);
break;
case OSEND:
- orderexpr(&r->left, out);
- orderexpr(&r->right, out);
+ orderexpr(&r->left, &l->n->ninit);
+ orderexpr(&r->right, &l->n->ninit);
break;
}
}
src/cmd/gc/select.c
--- a/src/cmd/gc/select.c
+++ b/src/cmd/gc/select.c
@@ -297,15 +297,15 @@ walkselect(Node *sel)
setlineno(cas);
n = cas->left;
r = nod(OIF, N, N);
- r->nbody = cas->ninit;
+ r->ninit = cas->ninit;
cas->ninit = nil;
if(n != nil) {
- r->nbody = concat(r->nbody, n->ninit);
+ r->ninit = concat(r->ninit, n->ninit);
n->ninit = nil;
}
if(n == nil) {
// selectdefault(sel *byte);
- r->ntest = mkcall("selectdefault", types[TBOOL], &init, var);
+ r->ntest = mkcall("selectdefault", types[TBOOL], &r->ninit, var);
} else {
switch(n->op) {
default:
@@ -313,25 +313,25 @@ walkselect(Node *sel)
case OSEND:
// selectsend(sel *byte, hchan *chan any, elem *any) (selected bool);
- n->left = safeexpr(n->left, &r->ninit);
+ n->left = localexpr(safeexpr(n->left, &r->ninit), n->left->type, &r->ninit);
n->right = localexpr(n->right, n->left->type->type, &r->ninit);
n->right = nod(OADDR, n->right, N);
n->right->etype = 1; // pointer does not escape
typecheck(&n->right, Erv);
r->ntest = mkcall1(chanfn("selectsend", 2, n->left->type), types[TBOOL],
- &init, var, n->left, n->right);
+ &r->ninit, var, n->left, n->right);
break;
case OSELRECV:
// selectrecv(sel *byte, hchan *chan any, elem *any) (selected bool);
r->ntest = mkcall1(chanfn("selectrecv", 2, n->right->left->type), types[TBOOL],
- &init, var, n->right->left, n->left);
+ &r->ninit, var, n->right->left, n->left);
break;
case OSELRECV2:
// selectrecv2(sel *byte, hchan *chan any, elem *any, received *bool) (selected bool);
r->ntest = mkcall1(chanfn("selectrecv2", 2, n->right->left->type), types[TBOOL],
- &init, var, n->right->left, n->left, n->ntest);
+ &r->ninit, var, n->right->left, n->left, n->ntest);
break;
}
}
src/cmd/gc/subr.c
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2040,11 +2040,13 @@ cheapexpr(Node *n, NodeList **init)
/*
* return n in a local variable of type t if it is not already.
+ * the value is guaranteed not to change except by direct
+ * assignment to it.
*/
Node*
localexpr(Node *n, Type *t, NodeList **init)
{
-\tif(n->op == ONAME &&\
+\tif(n->op == ONAME && !n->addrtaken &&\
(n->class == PAUTO || n->class == PPARAM || n->class == PPARAMOUT) &&\
convertop(n->type, t, nil) == OCONVNOP)\
return n;
test/fixedbugs/issue4313.go
// run
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Order of operations in select.
package main
func main() {
c := make(chan int, 1)
x := 0
select {
case c <- x: // should see x = 0, not x = 42 (after makec)
case <-makec(&x): // should be evaluated only after c and x on previous line
}
y := <-c
if y != 0 {
panic(y)
}
}
func makec(px *int) chan bool {
if false { for {} }
*px = 42
return make(chan bool, 0)
}
コアとなるコードの解説
このコミットの核心は、select
ステートメントの評価セマンティクスを正しく実装するために、コンパイラがチャネル操作の引数として使用される変数の値を適切に「固定」することです。
-
order.c
の変更:orderexpr(&r->right->left, out);
からorderexpr(&r->right->left, &l->n->ninit);
への変更は、OSELRECV2
およびOSEND
操作の引数(チャネルや送信値)の評価によって生成される初期化ステートメントが、select
ケース自身の初期化リスト (l->n->ninit
) に追加されるようにします。これにより、これらの引数の評価が、select
文全体の評価コンテキスト内で正しく順序付けられ、他のケースの副作用によって影響を受けないようになります。 -
select.c
の変更:r->nbody = cas->ninit;
からr->ninit = cas->ninit;
への変更は、select
ケースの初期化リストが、そのケースに対応するif
ステートメントの初期化リストに正しく伝播されることを保証します。これは、select
の内部変換において、初期化ステートメントが適切な場所に配置されるために重要です。mkcall
/mkcall1
の第3引数が&init
から&r->ninit
に変更されたのは、select
ランタイム関数呼び出しの引数(チャネル、送信値、受信バッファなど)の評価に伴う初期化ステートメントが、生成されるif
ステートメントの初期化リスト (r->ninit
) に追加されるようにするためです。これにより、これらの引数の値が、ランタイム関数が呼び出される前に確実に評価され、固定されます。OSEND
ケースにおけるn->left = localexpr(safeexpr(n->left, &r->ninit), n->left->type, &r->ninit);
の追加は特に重要です。これは、チャネル送信操作のチャネル式 (n->left
) が、localexpr
によってローカル変数に「固定」されることを意味します。safeexpr
は式が安全に評価されることを保証し、その結果がlocalexpr
に渡されます。localexpr
は、その値を一時変数にコピーすることで、チャネル式が評価された後、その値が他のselect
ケースの評価によって変更されることを防ぎます。
-
subr.c
の変更:localexpr
関数における!n->addrtaken
の追加は、localexpr
の挙動をより正確にします。以前は、ONAME
ノード(変数)がローカル変数であれば、その変数をそのまま返していました。しかし、もしその変数のアドレスが取得されている場合(n->addrtaken
がtrue)、その変数はポインタを介して間接的に変更される可能性があります。この変更により、localexpr
は、アドレスが取得された変数に対しては、新しい一時変数を作成して値をコピーするようになります。これにより、localexpr
が返す値が、後続の操作によって予期せず変更されることがなくなります。
test/fixedbugs/issue4313.go
は、このバグを再現するためのテストケースです。
main
関数内で、c <- x
と <-makec(&x)
の2つのケースを持つselect
文があります。
makec
関数は、引数として渡されたポインタpx
が指すx
の値を42
に変更します。
もしc <- x
のx
がselect
の評価中にmakec
によって変更されてしまうと、チャネルc
に送信される値が0
ではなく42
になってしまいます。
このコミットの修正により、c <- x
のx
の値はselect
が評価される前に「固定」されるため、makec
がx
の値を変更しても、チャネルc
には正しく0
が送信されるようになります。テストケースは、最終的にチャネルから受信した値が0
であることをアサートすることで、この修正が正しく機能することを確認しています。
関連リンク
- Go言語の
select
ステートメントに関する公式ドキュメント: https://go.dev/ref/spec#Select_statements - Go言語のメモリモデル: https://go.dev/ref/mem
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc
ディレクトリ): https://github.com/golang/go - Go Issue Tracker (GitHub): https://github.com/golang/go/issues (ただし、Issue #4313 は直接見つからなかったため、内部的なものか、古いトラッカーに存在した可能性があります。)
- Go Code Review (Gerrit): https://go.dev/cl/6997047 (コミットメッセージに記載されているGerritの変更リストへのリンク)
- Goコンパイラの内部構造に関する一般的な情報源 (例: "Go Compiler Internals" などの書籍やブログ記事)