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

[インデックス 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{}.ia[S{}.i]*a[S{}.i])とチャネル操作を含んでいます。修正前のコンパイラでは、このような複雑なポストコンディションが-raceモードで正しく計測されず、データ競合が発生しても検出されない可能性がありました。このテストは、-raceフラグを付けてコンパイルし、実行時にRace Detectorがクラッシュしたり、誤った警告を出したりすることなく、正しく動作することを確認します。テストのコメントにある「If this compiles with -race, the test passes.」は、コンパイルと実行が成功することが、このバグが修正されたことの証拠であることを示唆しています。

関連リンク

参考にした情報源リンク

  • Go Code Review (CL) 100870046のサマリー情報
  • Go言語の公式ドキュメント(Race Detector, forループの構文など)
  • Goコンパイラの内部構造に関する一般的な知識