[インデックス 19475] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
におけるデータ競合検出器(-race
モード)の挙動に関するバグ修正です。特に、for
ループのポストコンディション(ループの各イテレーションの最後に実行される部分)の処理が正しく行われず、データ競合が検出されない、または誤検出される可能性があった問題(Issue 8102)を解決します。この修正により、for
ループ内の複雑な式がrace
モードで適切に計測されるようになります。
コミット
cmd/gc: fix handling of for post-condition in -race mode
Fixes #8102.
LGTM=bradfitz, dvyukov
R=golang-codereviews, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/100870046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e56dc9966504405ecdad49f54edb45859ab3fa91
元コミット内容
Goコンパイラ(cmd/gc
)において、-race
モードでのfor
ループのポストコンディション(ループの各イテレーションの最後に実行される式)の処理に関するバグを修正します。この修正はIssue 8102に対応するものです。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための強力なツールである「データ競合検出器(Race Detector)」が組み込まれています。これは、コンパイル時に-race
フラグを付けてビルドすることで有効になります。Race Detectorは、実行時にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告を発します。
Issue 8102は、このRace Detectorがfor
ループの特定の構造、特にポストコンディション(例: for init; cond; post { ... }
のpost
部分)を正しく計測できないという問題でした。コンパイラがコードを計測(instrumentation)する際、for
ループのポストコンディション内の式が、そのループ本体や初期化部分とは異なる方法で扱われることがありました。これにより、ポストコンディション内で発生する可能性のあるデータ競合が検出されなかったり、あるいは誤った計測によってプログラムの挙動が変わってしまったりする可能性がありました。
このコミットは、for
ループのポストコンディションがRace Detectorによって適切に計測されるように、コンパイラのコード生成ロジックを修正することを目的としています。
前提知識の解説
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。Race Detectorの計測コードの挿入もこのコンパイラが行います。 - Go Race Detector (
-race
フラグ): Goランタイムに組み込まれたデータ競合検出ツールです。プログラムの実行中にデータ競合をリアルタイムで検出します。-race
フラグを付けてコンパイルすると、コンパイラはメモリアクセスを監視するための追加のコード(計測コード)を生成します。 - データ競合 (Data Race): 複数の並行実行されるゴルーチンが、同期メカニズムなしに同じメモリ位置に同時にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生する競合状態です。データ競合はプログラムの予測不能な挙動やバグの原因となります。
for
ループの構造: Goのfor
ループはC言語のそれと似ており、for 初期化; 条件; 後処理 { ... }
の形式を取ることができます。初期化 (init)
: ループ開始前に一度だけ実行されます。条件 (cond)
: 各イテレーションの開始時に評価され、true
の場合にループ本体が実行されます。後処理 (post)
: 各イテレーションの終了時に、条件が再評価される前に実行されます。
- AST (Abstract Syntax Tree): コンパイラがソースコードを解析して生成する、プログラムの構造を木構造で表現したものです。コンパイラはASTを操作して最適化やコード生成を行います。Race Detectorの計測もASTの段階で行われます。
racewalknode
関数:cmd/gc
内の関数で、Race Detectorの計測処理においてASTノードを走査し、必要に応じて計測コードを挿入します。
技術的詳細
Goコンパイラのcmd/gc
は、ソースコードをASTに変換した後、racewalk.c
内のracewalknode
関数などを用いてRace Detectorの計測コードを挿入します。この計測は、メモリへの読み書き操作の前後に行われ、実行時に競合を検出するための情報(アクセスアドレス、ゴルーチンID、スタックトレースなど)を記録します。
問題は、for
ループのポストコンディションが、コンパイラの内部表現において、ループ本体とは異なるNodeList
(ASTノードのリスト)として扱われることにありました。特に、x, y := f()
のような多値代入を含む式がBLOCK{CALL f, AS x [SP+0], AS y [SP+n]}
のような内部表現に変換される場合、これらのステートメント間に計測コードを挿入すると、一時的な結果が「破壊される(smash the results)」可能性がありました。
既存のコードでは、racewalknode(&n->list->n, &n->ninit, 0, 0);
という呼び出しがありましたが、これはn->ninit
という誤った初期化リストを渡していました。for
ループのポストコンディションは、実際にはn->list->n
(ループの条件や本体に関連するノード)の初期化リストに属するべきでした。この誤った参照により、ポストコンディション内の式がRace Detectorによって適切に計測されず、データ競合が見過ごされる可能性がありました。
修正は、racewalknode
の呼び出しで渡す初期化リストのポインタを&n->ninit
から&n->list->n->ninit
に変更することで、ポストコンディションが属する正しいASTノードの初期化リストに計測コードが挿入されるようにしました。これにより、for
ループのポストコンディション内の複雑な式も、他のコードと同様にRace Detectorの監視対象となり、正確なデータ競合検出が可能になります。
コアとなるコードの変更箇所
src/cmd/gc/racewalk.c
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -182,7 +182,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)\n // x, y := f() becomes BLOCK{CALL f, AS x [SP+0], AS y [SP+n]}\n // We don't want to instrument between the statements because it will\n // smash the results.\n-\t\t\tracewalknode(&n->list->n, &n->ninit, 0, 0);\n+\t\t\tracewalknode(&n->list->n, &n->list->n->ninit, 0, 0);\n fini = nil;\n racewalklist(n->list->next, &fini);\n n->list = concat(n->list, fini);\
src/pkg/runtime/race/race_test.go
--- a/src/pkg/runtime/race/race_test.go
+++ b/src/pkg/runtime/race/race_test.go
@@ -155,3 +155,18 @@ func runTests() ([]byte, error) {
cmd.Env = append(cmd.Env, `GORACE="suppress_equal_stacks=0 suppress_equal_addresses=0 exitcode=0"`)
return cmd.CombinedOutput()
}
+
+func TestIssue8102(t *testing.T) {
+ // If this compiles with -race, the test passes.
+ type S struct {
+ x interface{}
+ i int
+ }
+ c := make(chan int)
+ a := [2]*int{}
+ for ; ; c <- *a[S{}.i] {
+ if t != nil {
+ break
+ }
+ }
+}
コアとなるコードの解説
src/cmd/gc/racewalk.c
の変更
変更の中心は、racewalknode
関数の呼び出しにおける第2引数です。
- 変更前:
racewalknode(&n->list->n, &n->ninit, 0, 0);
- 変更後:
racewalknode(&n->list->n, &n->list->n->ninit, 0, 0);
この変更は、for
ループのポストコンディション(n->list->n
が指すノードに関連する部分)の計測において、正しい初期化リスト(n->list->n->ninit
)を渡すように修正しています。これにより、ポストコンディション内の式が、その式が属するASTノードの正しいコンテキストで計測されるようになり、Race Detectorが正確に動作するようになります。コメントにあるように、x, y := f()
のような多値代入がBLOCK{CALL f, AS x [SP+0], AS y [SP+n]}
のように展開される際に、これらの内部ステートメント間に計測コードが誤って挿入されることを防ぎつつ、必要な計測は行われるように調整されています。
src/pkg/runtime/race/race_test.go
の追加テスト
TestIssue8102
という新しいテストケースが追加されました。このテストは、問題が修正されたことを検証するためのものです。
func TestIssue8102(t *testing.T) {
// If this compiles with -race, the test passes.
type S struct {
x interface{}
i int
}
c := make(chan int)
a := [2]*int{}
for ; ; c <- *a[S{}.i] {
if t != nil {
break
}
}
}
このテストの核心は、for ; ; c <- *a[S{}.i]
というfor
ループの構造です。
- 初期化部分と条件部分は省略されています。
- ポストコンディションに
c <- *a[S{}.i]
という複雑な式が含まれています。S{}.i
は構造体のゼロ値のi
フィールド(int
型なので0
)にアクセスします。a[S{}.i]
は配列a
のインデックス0
の要素(*int
型)にアクセスします。*a[S{}.i]
はそのポインタが指す値にデリファレンスします。c <- ...
はその値をチャネルc
に送信します。
このポストコンディションは、複数のメモリアクセス(S{}.i
、a[S{}.i]
、*a[S{}.i]
)とチャネル操作を含んでいます。修正前のコンパイラでは、このような複雑なポストコンディションが-race
モードで正しく計測されず、データ競合が発生しても検出されない可能性がありました。このテストは、-race
フラグを付けてコンパイルし、実行時にRace Detectorがクラッシュしたり、誤った警告を出したりすることなく、正しく動作することを確認します。テストのコメントにある「If this compiles with -race, the test passes.」は、コンパイルと実行が成功することが、このバグが修正されたことの証拠であることを示唆しています。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/e56dc9966504405ecdad49f54edb45859ab3fa91
- Go Code Review (CL): https://golang.org/cl/100870046
- Go Issue 8102 (CLのサマリーから推測される情報):
cmd/gc: fix handling of for post-condition in -race mode
に関連する問題。
参考にした情報源リンク
- Go Code Review (CL) 100870046のサマリー情報
- Go言語の公式ドキュメント(Race Detector, forループの構文など)
- Goコンパイラの内部構造に関する一般的な知識