[インデックス 14742] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)におけるレース検出器の計装(instrumentation)に関するバグ修正です。具体的には、アドレスを持たない(unaddressable)配列に対するアクセスが正しくレース検出器によって計装されない問題を解決します。これにより、Map[k][i]
のような形式でアクセスされる配列要素におけるデータ競合が適切に検出されるようになります。
コミット
commit ecbf99ad975970bd3496880ebf6bc8a2d19b31eb
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Dec 24 12:14:41 2012 +0100
cmd/gc: fix race instrumentation of unaddressable arrays.
Fixes #4578.
R=dvyukov, golang-dev
CC=golang-dev
https://golang.org/cl/7005050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ecbf99ad975970bd3496880ebf6bc8a2d19b31eb
元コミット内容
cmd/gc: fix race instrumentation of unaddressable arrays.
Fixes #4578.
R=dvyukov, golang-dev
CC=golang-dev
https://golang.org/cl/7005050
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための「レース検出器(Race Detector)」が組み込まれています。これは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態を特定する強力なツールです。
このコミット以前は、特定の状況下でレース検出器が正しく機能しないバグが存在していました。特に問題となっていたのは、「アドレスを持たない(unaddressable)配列」へのアクセスです。Go言語では、マップの値として格納された配列のように、直接メモリアドレスを持たない値が存在します。例えば、m := make(map[int][2]int)
のようなマップ m
があり、_ = m[1][1]
のようにアクセスする場合、m[1]
はマップからコピーされた値であり、その要素 m[1][1]
もまたアドレスを持ちません。
このようなアドレスを持たない配列要素へのアクセスに対して、レース検出器が適切な計装コード(instrumentation code)を挿入できていなかったため、本来検出されるべきデータ競合が見過ごされる可能性がありました。このコミットは、この計装の漏れを修正し、レース検出器の信頼性を向上させることを目的としています。コミットメッセージにある #4578
は、この問題が報告されたGoのIssueトラッカーの番号を指しています。
前提知識の解説
Go Race Detector (レース検出器)
Goのレース検出器は、Goプログラムの実行中にデータ競合を動的に検出するツールです。これは、コンパイル時に特別な計装コードをプログラムに挿入することで機能します。この計装コードは、メモリへのアクセス(読み込みと書き込み)を監視し、複数のゴルーチンが競合するアクセスを行った場合に警告を発します。レース検出器は、go run -race
や go build -race
、go test -race
のように -race
フラグを付けてGoコマンドを実行することで有効になります。
Race Conditions (データ競合)
データ競合は、並行プログラミングにおけるバグの一種で、以下の3つの条件がすべて満たされたときに発生します。
- 少なくとも2つのゴルーチンが同時に同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- これらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合は、プログラムの非決定的な動作やクラッシュを引き起こす可能性があり、デバッグが非常に困難な種類のバグです。
Addressable vs. Unaddressable Values (アドレスを持つ値と持たない値)
Go言語では、すべての値がメモリアドレスを持つわけではありません。
- アドレスを持つ値 (Addressable Values): 変数、ポインタが指す値、スライス要素、構造体のフィールドなど、メモリ上に明確な場所を持つ値です。これらの値は
&
演算子を使ってそのアドレスを取得できます。 - アドレスを持たない値 (Unaddressable Values): 定数、リテラル、マップの要素(マップの値はコピーされるため)、関数呼び出しの結果、型変換の結果など、一時的な値やメモリ上に固定された場所を持たない値です。これらの値に対しては
&
演算子を使用できません。
レース検出器はメモリへのアクセスを監視するため、アドレスを持つ値に対しては直接計装を適用できますが、アドレスを持たない値、特にその内部に含まれる構造体や配列の要素へのアクセスは、計装が複雑になります。
cmd/gc
cmd/gc
はGo言語の公式コンパイラです。Goのソースコードを機械語に変換する主要なツールチェーンの一部です。レース検出器の計装は、このコンパイラの段階でコードに挿入されます。
racewalk.c
racewalk.c
は、Goコンパイラ(cmd/gc
)のソースコードの一部で、レース検出器の計装ロジックを実装しています。このファイルは、Goプログラムの抽象構文木(AST)を走査し、メモリアクセスが発生する箇所にレース検出のための特別な関数呼び出し(callinstr
)を挿入します。
OINDEX
OINDEX
は、Goコンパイラの内部表現(ASTノード)で、配列やスライス、またはマップのインデックスアクセス操作を表します。例えば、a[i]
や m[k]
のような式は OINDEX
ノードとして表現されます。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのレース検出器がどのようにメモリアクセスを計装するか、そしてアドレスを持たない配列のインデックスアクセスをどのように扱うかに関わっています。
レース検出器は、プログラム内の各メモリアクセス(読み込みまたは書き込み)に対して、runtime.racefuncenter
や runtime.racefuncexit
のような関数呼び出しを挿入します。これらの関数は、実行時にメモリのアクセス履歴を追跡し、データ競合を検出します。
問題は、Map[k][i]
のような式で発生していました。ここで Map[k]
はマップから取得された値であり、これはアドレスを持たないコピーです。もし Map[k]
が配列型であった場合、その後の [i]
によるインデックスアクセスは、アドレスを持たない配列の要素へのアクセスとなります。
従来の racewalk.c
のロジックでは、OINDEX
ノードを処理する際に、インデックスされる対象(n->left
)が固定長配列(isfixedarray
)であり、かつアドレスを持つ値(islvalue
)である場合にのみ、その要素へのアクセスを計装していました。しかし、Map[k]
のようにアドレスを持たない配列の場合、islvalue(n->left)
が false
となり、計装がスキップされてしまっていました。
このコミットは、このロジックに新しい条件を追加します。n->left
が固定長配列であり、かつアドレスを持たない場合(!islvalue(n->left)
)、特別な処理を行うように変更されました。この場合、n->left
(アドレスを持たない配列自体)と n->right
(インデックス)の両方に対して racewalknode
を再帰的に呼び出し、適切な計装が行われるようにします。これにより、アドレスを持たない配列の要素へのアクセスもレース検出器の監視対象となります。
具体的には、Map[k][i]
のようなケースでは、n->left
が Map[k]
に相当し、これはアドレスを持たない配列です。この修正により、Map[k]
自体とインデックス i
の両方が適切に処理され、最終的に Map[k][i]
へのアクセスがレース検出器によって監視されるようになります。
また、テストケース TestRaceMapRWArray
が追加され、この修正が正しく機能することを確認しています。このテストは、マップの値として配列を使用し、異なるゴルーチンから同時に読み書きを行うことで、データ競合が発生する状況を再現しています。
コアとなるコードの変更箇所
変更は主に2つのファイルで行われています。
-
src/cmd/gc/racewalk.c
racewalknode
関数内のOINDEX
処理に新しいelse if
ブロックが追加されました。- コメント行が修正されました。
-
src/pkg/runtime/race/testdata/map_test.go
TestRaceMapRWArray
という新しいテスト関数が追加されました。
src/cmd/gc/racewalk.c
の変更点
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -282,6 +282,12 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)\n \tcase OINDEX:\n \t\tif(!isfixedarray(n->left->type))\n \t\t\tracewalknode(&n->left, init, 0, 0);\n+\t\telse if(!islvalue(n->left)) {\n+\t\t\t// index of unaddressable array, like Map[k][i].\n+\t\t\tracewalknode(&n->left, init, wr, 0);\n+\t\t\tracewalknode(&n->right, init, 0, 0);\n+\t\t\tgoto ret;\n+\t\t}\n \t\tracewalknode(&n->right, init, 0, 0);\n \t\tif(n->left->type->etype != TSTRING)\n \t\t\tcallinstr(&n, init, wr, skip);\
@@ -422,7 +428,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)\n \tint class, res, hascalls;\n \n \tn = *np;\n-\t//print(\"callinstr for %+N [ %O ] etype=%d class=%d\\n\",\n+\t//print(\"callinstr for %+N [ %O ] etype=%E class=%d\\n\",\n \t//\t n, n->op, n->type ? n->type->etype : -1, n->class);\
src/pkg/runtime/race/testdata/map_test.go
の変更点
--- a/src/pkg/runtime/race/testdata/map_test.go
+++ b/src/pkg/runtime/race/testdata/map_test.go
@@ -30,6 +30,18 @@ func TestRaceMapRW2(t *testing.T) {\n \t<-ch\n }\n \n+func TestRaceMapRWArray(t *testing.T) {\n+\t// Check instrumentation of unaddressable arrays (issue 4578).\n+\tm := make(map[int][2]int)\n+\tch := make(chan bool, 1)\n+\tgo func() {\n+\t\t_ = m[1][1]\n+\t\tch <- true\n+\t}()\n+\tm[2] = [2]int{1, 2}\n+\t<-ch\n+}\n+\n func TestNoRaceMapRR(t *testing.T) {\n \tm := make(map[int]int)\n \tch := make(chan bool, 1)\
コアとなるコードの解説
src/cmd/gc/racewalk.c
の変更解説
racewalknode
関数は、GoのASTノードを走査し、レース検出器の計装を挿入する役割を担っています。OINDEX
(インデックスアクセス)ノードの処理において、以下の新しいロジックが追加されました。
else if(!islvalue(n->left)) {
// index of unaddressable array, like Map[k][i].
racewalknode(&n->left, init, wr, 0);
racewalknode(&n->right, init, 0, 0);
goto ret;
}
else if(!islvalue(n->left))
: この条件は、インデックスアクセスされる対象(n->left
)がアドレスを持たない値である場合に真となります。これは、Map[k][i]
のMap[k]
のようなケースを捕捉します。Map[k]
はマップからコピーされた配列であり、それ自体はアドレスを持ちません。// index of unaddressable array, like Map[k][i].
: このコメントは、このブロックがアドレスを持たない配列のインデックスアクセスを処理するためのものであることを明確に示しています。racewalknode(&n->left, init, wr, 0);
: アドレスを持たない配列自体(n->left
)に対してもracewalknode
を再帰的に呼び出します。これにより、この配列へのアクセスが適切に計装されます。wr
は書き込みアクセスかどうかを示すフラグです。racewalknode(&n->right, init, 0, 0);
: インデックス(n->right
)に対してもracewalknode
を再帰的に呼び出します。インデックス自体は通常読み込みアクセスであり、その値の評価も計装の対象となる場合があります。goto ret;
: このgoto
ステートメントは、この特定のケースの処理が完了したことを示し、racewalknode
関数の残りの部分をスキップして、関数の終了処理にジャンプします。これにより、アドレスを持たない配列のインデックスアクセスに対する計装が重複して行われるのを防ぎます。
この変更により、Map[k][i]
のような式が評価される際に、Map[k]
の配列部分へのアクセスと、その後の [i]
による要素アクセスが、レース検出器によって正しく監視されるようになります。
また、コメント行の修正は、デバッグ出力のフォーマットを改善するためのものです。etype=%d
が etype=%E
に変更されており、これは型情報をより詳細に表示するための変更と考えられます。
src/pkg/runtime/race/testdata/map_test.go
の変更解説
TestRaceMapRWArray
は、この修正が正しく機能することを検証するための新しいテストケースです。
func TestRaceMapRWArray(t *testing.T) {
// Check instrumentation of unaddressable arrays (issue 4578).
m := make(map[int][2]int)
ch := make(chan bool, 1)
go func() {
_ = m[1][1] // Goroutine 1: Reads an element of an unaddressable array
ch <- true
}()
m[2] = [2]int{1, 2} // Main goroutine: Writes to a different element of the map, potentially causing a race if not handled correctly
<-ch
}
m := make(map[int][2]int)
: キーがint
、値が要素数2のint
型配列であるマップを作成します。マップの値はコピーされるため、m[key]
で取得される配列はアドレスを持ちません。go func() { _ = m[1][1]; ch <- true }()
: 新しいゴルーチンを起動し、マップm
のキー1
に対応する配列の2番目の要素(インデックス1
)を読み込みます。このm[1]
はアドレスを持たない配列です。m[2] = [2]int{1, 2}
: メインのゴルーチンで、マップm
のキー2
に新しい配列を書き込みます。
このテストの意図は、m[1][1]
の読み込みと m[2]
の書き込みが、異なるマップエントリに対する操作であるため、本来はデータ競合が発生しないことを確認することです。しかし、もし m[1][1]
のようなアドレスを持たない配列要素へのアクセスが正しく計装されていなかった場合、レース検出器が誤って競合を報告したり、あるいは本来検出されるべき競合を見過ごしたりする可能性がありました。このテストは、修正後のレース検出器がこのようなケースを正しく処理し、不必要な競合を報告しないことを確認します。
関連リンク
参考にした情報源リンク
- Go言語の公式ドキュメント(Race Detectorに関する情報)
- Go言語のコンパイラソースコード(
src/cmd/gc/racewalk.c
) - Go言語のランタイムソースコード(
src/pkg/runtime/race/testdata/map_test.go
) - Go言語におけるアドレス可能性に関する一般的な情報
- Go Issue #4578 (直接的な情報は見つかりませんでしたが、コミットメッセージから問題の性質を推測しました)