[インデックス 17653] ファイルの概要
このコミットは、Goコンパイラのcmd/gcにおけるswitchステートメントの処理に関するバグ修正です。具体的には、switchステートメントの抽象構文木(AST)ノードが、コンパイルのウォーク(走査)フェーズ後に適切にクリーンアップされないために、Goのレース検出器が誤動作する問題に対処しています。この変更により、古いAST要素へのポインタがクリアされ、レース検出器の正確性が向上します。
コミット
- コミットハッシュ:
381b72a7a3cc4c7182319f84297d40bb7b459dc4 - Author: Rémy Oudompheng oudomphe@phare.normalesup.org
- Date: Thu Sep 19 09:23:04 2013 +0200
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/381b72a7a3cc4c7182319f84297d40bb7b459dc4
元コミット内容
cmd/gc: cleanup SWITCH nodes after walk.
Keeping pointers from the pre-walk phase confuses
the race detection instrumentation.
Fixes #6418.
R=golang-dev, dvyukov, r
CC=golang-dev
https://golang.org/cl/13368057
変更の背景
この変更は、Goのコンパイラ(cmd/gc)がswitchステートメントを処理する際に発生する特定の問題を解決するために行われました。問題の根源は、コンパイルプロセスにおける「ウォーク(walk)」フェーズの後に、switchステートメントを表す抽象構文木(AST)ノードが適切にクリーンアップされないことにありました。
具体的には、ウォークフェーズ以前のAST要素へのポインタが残存していると、Goの並行処理におけるデータ競合を検出するための「レース検出器(Race Detector)」が混乱し、誤った検出結果を出す可能性がありました。これは、レース検出器がメモリへのアクセスパターンを監視する際に、古い、もはや有効ではないポインタが参照されることで、実際には存在しない競合を報告してしまうためです。
この問題は、GoのIssue #6418として報告されており、このコミットはその問題を修正することを目的としています。レース検出器はGoの並行プログラミングにおける重要なデバッグツールであるため、その正確性を確保することは非常に重要です。
前提知識の解説
Goコンパイラ (cmd/gc)
cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスは複数のフェーズに分かれており、構文解析、型チェック、最適化、コード生成などが含まれます。このコミットが関連するのは、主に構文解析後の抽象構文木(AST)の処理と、その後のコード生成に関連する部分です。
抽象構文木 (AST)
抽象構文木(Abstract Syntax Tree, AST)は、ソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラはソースコードを読み込むと、まずこのASTを生成します。ASTは、プログラムの構造を表現し、コンパイラの各フェーズ(型チェック、最適化、コード生成など)で利用されます。
walkswitch 関数
walkswitch関数は、cmd/gcコンパイラのsrc/cmd/gc/swt.cファイルに存在する関数で、Go言語のswitchステートメントのASTノードを処理する役割を担っています。この関数は、switchステートメントの条件式や各case節を走査し、それらをコンパイルに適した形に変換します。
Goのレース検出器 (Race Detector)
Goのレース検出器は、Goプログラムにおけるデータ競合(data race)を検出するための強力なツールです。データ競合とは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があります。
レース検出器は、プログラムの実行時にメモリへのアクセスを監視し、競合のパターンを特定します。このツールは、コンパイル時に-raceフラグを付けてビルドすることで有効になります(例: go run -race main.go)。レース検出器は、メモリへのアクセスをフックし、アクセス履歴を記録することで機能します。この履歴に基づいて、競合が発生した可能性のある場所を特定し、詳細なレポートを出力します。
SWITCH ノード
SWITCHノードは、GoコンパイラのASTにおいてswitchステートメントを表すデータ構造です。このノードは、switchステートメントの条件式(ntestフィールド)や、各case節のリスト(listフィールド)などの情報を含んでいます。
技術的詳細
このコミットの技術的な核心は、switchステートメントのASTノード(Node *sw)がwalkswitch関数によって処理された後、そのノードが保持していた一部のポインタ(sw->ntestとsw->list)がクリアされずに残っていた点にあります。
walkswitch関数は、switchステートメントのASTを走査し、必要に応じて変換や最適化を行います。この処理が完了すると、元のASTノードの一部はもはや必要なくなり、新しい、変換されたAST構造が使用されます。しかし、古いAST要素へのポインタが残っていると、Goのレース検出器がこれらの「古い」ポインタを追跡し続ける可能性がありました。
レース検出器は、メモリへのアクセスを監視し、そのアクセスが並行して行われた場合に競合を検出します。もし、コンパイラがすでに処理を終え、もはや有効ではないAST要素へのポインタが残っていると、レース検出器はこれらのポインタが指すメモリ領域へのアクセスを監視し続けることになります。そして、もし別のゴルーチンがそのメモリ領域にアクセスした場合、レース検出器は誤ってデータ競合を報告してしまう可能性がありました。これは、実際にはプログラムのロジック上問題ないアクセスであっても、古いポインタが参照されているために誤検出されるという状況です。
このコミットでは、walkswitch関数内でexprswitch(sw)が呼び出され、switchステートメントの式が処理された直後に、sw->ntest = nil;とsw->list = nil;という行を追加することで、この問題を解決しています。これにより、switchノードが保持していた条件式とケースリストへのポインタが明示的にnilに設定され、古いAST要素への参照が断ち切られます。結果として、レース検出器が誤ったポインタを追跡することがなくなり、その正確性が保証されるようになります。
また、この変更を検証するために、src/pkg/runtime/race/testdata/mop_test.goにTestRaceCaseIssue6418という新しいテストケースが追加されました。このテストケースは、マップ操作とswitchステートメントを組み合わせることで、以前にレース検出器が誤検出を引き起こしていた状況を再現し、今回の修正が正しく機能することを確認します。
コアとなるコードの変更箇所
src/cmd/gc/swt.cのwalkswitch関数に以下の変更が加えられました。
--- a/src/cmd/gc/swt.c
+++ b/src/cmd/gc/swt.c
@@ -820,6 +820,9 @@ walkswitch(Node *sw)
return;
}
exprswitch(sw);
+ // Discard old AST elements after a walk. They can confuse racewealk.
+ sw->ntest = nil;
+ sw->list = nil;
}
/*
また、src/pkg/runtime/race/testdata/mop_test.goに以下のテストケースが追加されました。
--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -231,6 +231,22 @@ func TestRaceCaseFallthrough(t *testing.T) {
<-ch
}
+func TestRaceCaseIssue6418(t *testing.T) {
+ m := map[string]map[string]string{
+ "a": map[string]string{
+ "b": "c",
+ },
+ }
+ ch := make(chan int)
+ go func() {
+ m["a"]["x"] = "y"
+ ch <- 1
+ }()
+ switch m["a"]["b"] {
+ }
+ <-ch
+}
+
func TestRaceCaseType(t *testing.T) {
var x, y int
var i interface{} = x
コアとなるコードの解説
src/cmd/gc/swt.cの変更
walkswitch関数は、switchステートメントのASTノードswを受け取り、その内部を走査して処理します。
変更が加えられたのは、exprswitch(sw);の呼び出し後です。
exprswitch(sw);
// Discard old AST elements after a walk. They can confuse racewealk.
sw->ntest = nil;
sw->list = nil;
exprswitch(sw);: この行は、switchステートメントの条件式を処理し、必要に応じてASTを変換する役割を担っています。この呼び出しが完了すると、元のsw->ntest(条件式)やsw->list(ケースリスト)が指していたAST要素は、もはやコンパイラの後続フェーズでは直接使用されなくなります。// Discard old AST elements after a walk. They can confuse racewealk.: このコメントは、なぜこれらのポインタをクリアするのかを明確に説明しています。「ウォーク後に古いAST要素を破棄する。これらはレース検出器を混乱させる可能性がある。」という意図が示されています。sw->ntest = nil;:switchステートメントの条件式を表すノードへのポインタntestをnilに設定します。これにより、古い条件式ASTへの参照が解除されます。sw->list = nil;:switchステートメントの各case節のリストを表すノードへのポインタlistをnilに設定します。これにより、古いケースリストASTへの参照が解除されます。
これらの変更により、walkswitch関数が完了した後、swノードが古い、もはや有効ではないAST要素へのポインタを保持しなくなり、レース検出器がこれらのポインタを誤って追跡することを防ぎます。
src/pkg/runtime/race/testdata/mop_test.goの変更
追加されたTestRaceCaseIssue6418テストケースは、このバグを再現し、修正が正しく機能することを確認するために設計されています。
func TestRaceCaseIssue6418(t *testing.T) {
m := map[string]map[string]string{
"a": map[string]string{
"b": "c",
},
}
ch := make(chan int)
go func() {
m["a"]["x"] = "y"
ch <- 1
}()
switch m["a"]["b"] {
}
<-ch
}
このテストケースのポイントは以下の通りです。
m := map[string]map[string]string{...}: ネストされたマップを初期化します。マップは並行アクセスにおいてデータ競合が発生しやすいデータ構造です。go func() { m["a"]["x"] = "y"; ch <- 1 }(): 新しいゴルーチンを起動し、その中でマップmに書き込みを行います。この書き込みは、メインゴルーチンがswitchステートメントを実行している間に発生する可能性があります。switch m["a"]["b"] { }: メインゴルーチンでswitchステートメントを実行します。このswitchステートメントは、マップmから値を読み取ります。この読み取りと、別のゴルーチンからのマップへの書き込みが同時に行われることで、データ競合の条件が満たされる可能性があります。<-ch: 両方のゴルーチンが完了するのを待機します。
このテストケースは、以前のバージョンではレース検出器が誤って競合を報告していた状況を再現し、今回の修正が適用されたGoコンパイラでは、この誤検出が解消されることを確認します。
関連リンク
- Go Issue #6418: このコミットが修正した問題のGoプロジェクトのIssueトラッカー上のエントリ。GoのIssueトラッカーは、Goプロジェクトのバグ報告や機能要望を管理するためのシステムです。
- Go Code Review (CL) 13368057: この変更がGoのコードレビューシステム(Gerrit)上でレビューされた際のリンク。Goプロジェクトでは、すべてのコード変更がGerritを通じてレビューされます。
参考にした情報源リンク
- コミットメッセージと差分情報
- Go言語の公式ドキュメント(Goコンパイラ、レース検出器に関する一般的な知識)
- 抽象構文木(AST)に関する一般的なプログラミング言語のコンパイラの知識