[インデックス 16300] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるデータ競合検出器のインストゥルメンテーションに関するバグ修正です。具体的には、T(v).Field
のようなセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を解決します。この修正により、Goのデータ競合検出器がより堅牢になり、特定のコードパターンにおける競合状態を見逃すことがなくなります。
コミット
commit 6c4943cb51cc7c4b27233a7717d74742871f7faa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed May 15 01:25:20 2013 +0200
cmd/gc: fix race instrumentation of selectors T(v).Field
Fixes #5424.
R=golang-dev, daniel.morsing, dvyukov, r
CC=golang-dev
https://golang.org/cl/9033048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6c4943cb51cc7c4b27233a7717d74742871f7faa
元コミット内容
このコミットは、Goコンパイラのcmd/gc
部分におけるデータ競合検出器のインストゥルメンテーションに関する修正です。特に、T(v).Field
という形式のセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を修正します。これは、GoのIssue #5424で報告されたバグに対応するものです。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための組み込みの競合検出器(Race Detector)があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、その検出は並行プログラムのデバッグにおいて非常に重要です。
このコミット以前は、T(v).Field
のような特定のコードパターン、すなわち型変換(T(v)
)の後にその結果の構造体のフィールドにアクセスする(.Field
)場合、競合検出器がそのメモリアクセスを正しくインストゥルメント(計測)できていませんでした。インストゥルメンテーションとは、コンパイラが実行時にメモリアクセスを監視するための追加コードを挿入するプロセスを指します。正しくインストゥルメントされない場合、実際にデータ競合が発生していても検出器がそれを報告しないという問題が生じます。
Issue #5424では、この問題が具体的に報告されており、T(v).Field
のような式が、競合検出器にとって「アドレス可能(addressable)」ではないと誤って判断されることが原因でした。Goの競合検出器は、メモリ位置のアドレスを取得してそのアクセスを記録するため、アドレス可能でない式に対してはインストゥルメンテーションを行うことができません。このコミットは、この特定のエッジケースを修正し、競合検出器の信頼性を向上させることを目的としています。
前提知識の解説
Go言語のデータ競合検出器 (Race Detector)
Goのデータ競合検出器は、Go 1.1で導入された強力なツールです。go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。有効にすると、コンパイラはプログラムのメモリアクセスを監視するための追加コードを挿入します。実行時に、この監視コードが並行アクセスを検出し、データ競合の可能性を報告します。これは、スレッドサニタイザー(ThreadSanitizer, TSan)という技術に基づいています。
cmd/gc
cmd/gc
は、Go言語の公式コンパイラのバックエンド部分を指します。Goのソースコードを機械語に変換する主要な役割を担っています。競合検出器のインストゥルメンテーションは、このコンパイラのフェーズで行われます。具体的には、racewalk.c
のようなファイルが、メモリアクセスを検出して競合検出器のランタイム関数呼び出しを挿入する役割を担っています。
セレクタ (Selectors)
Go言語において、セレクタは構造体のフィールドやメソッドにアクセスするための構文です。例えば、s.Field
やs.Method()
のように使用されます。このコミットで問題となっているのは、T(v).Field
のような形式です。ここでT
は型、v
は変数です。これは、v
を型T
に変換した後に、その結果のフィールドField
にアクセスすることを意味します。
アドレス可能性 (Addressability)
Go言語において、ある式が「アドレス可能」であるとは、その式のメモリ上の位置(アドレス)を取得できることを意味します。アドレス可能である式に対しては、&
演算子を使用してポインタを取得できます。例えば、変数や配列の要素、構造体のフィールドは通常アドレス可能です。しかし、定数、リテラル、関数呼び出しの結果、マップの要素などはアドレス可能ではありません。競合検出器は、メモリアクセスを監視するためにそのアドレスを必要とするため、アドレス可能でない式に対するアクセスは直接インストゥルメントできません。
OCONVNOP
Goコンパイラの内部表現において、OCONVNOP
は「no-op conversion」(何もしない変換)を表すノードタイプです。これは、型変換が行われるが、実行時には実際の値の変更やコピーが発生しないようなケース(例えば、基底型が同じエイリアス型への変換など)で生成されることがあります。このコミットでは、T(v).Field
のようなケースで、T(v)
の部分がOCONVNOP
として表現されることがあり、これが競合検出器のインストゥルメンテーションを妨げていた可能性があります。
技術的詳細
このコミットの核心は、src/cmd/gc/racewalk.c
ファイルに追加されたmakeaddable
関数と、そのcallinstr
関数からの呼び出しです。
racewalk.c
の役割
racewalk.c
は、Goコンパイラのデータ競合検出器のインストゥルメンテーションロジックが含まれるファイルです。このファイル内の関数は、プログラムの抽象構文木(AST)を走査し、メモリ読み書き操作を特定します。そして、これらの操作の直前に、競合検出器のランタイム関数(raceread
やracewrite
)への呼び出しを挿入します。これらのランタイム関数は、アクセスされたメモリのアドレスとサイズを記録し、他のゴルーチンからのアクセスと照合して競合を検出します。
makeaddable
関数の導入
makeaddable
関数は、n
というノード(ASTの要素)を受け取り、そのメモリ位置がn
と同じでありながら、Go言語の意味で「アドレス可能」なノードに変換することを目的としています。これは、cheapexpr
のような関数が引数のコピーを作成する可能性があるのとは異なり、元のメモリ位置を維持しつつアドレス可能性を確保します。
makeaddable
関数は、以下のノードタイプに対して特別な処理を行います。
OINDEX
(配列/スライス要素へのアクセス):- もし
n->left->type
が固定長配列(isfixedarray
)であれば、配列の基底部分(n->left
)もアドレス可能にするために再帰的にmakeaddable(n->left)
を呼び出します。これは、array[index]
のようなアクセスで、array
自体が型変換の結果である場合に重要になります。
- もし
ODOT
(構造体フィールドへのアクセス) およびOXDOT
(ポインタ経由の構造体フィールドへのアクセス):T(v).Field
のようなケースでは、T(v)
の部分がコンパイラ内部でOCONVNOP
ノードとして表現されることがあります。OCONVNOP
は実行時に何もしない変換ですが、競合検出器にとっては「アドレス可能でない」と誤解される原因となっていました。makeaddable
は、n->left->op == OCONVNOP
の場合、n->left
をn->left->left
に直接置き換えます。これにより、T(v)
という変換ノードをスキップし、元の変数v
に直接アクセスする形にASTを書き換えます。これにより、v.Field
という形になり、v
がアドレス可能であれば、そのフィールドもアドレス可能と判断されるようになります。- その後、
makeaddable(n->left)
を再帰的に呼び出し、フィールドアクセス元のノード(変換を剥がした後のv
)もアドレス可能であることを確認します。
ODOTPTR
(ポインタ経由の構造体フィールドへのアクセス):- このケースでは、特に何もする必要がないと判断されています。ポインタ経由のアクセスは通常、既にアドレス可能であるためです。
default
:- 上記以外のノードタイプでは、特に何も処理を行いません。
callinstr
関数での利用
callinstr
関数は、メモリ読み書き操作をインストゥルメントする主要な関数です。この関数内で、uintptraddr(n)
を呼び出す前に、新しく追加されたmakeaddable(n)
が呼び出されます。これにより、uintptraddr
(メモリのアドレスを取得する関数)に渡されるノードn
が、Go言語の意味でアドレス可能であることが保証されます。結果として、T(v).Field
のようなケースでも、競合検出器が正しくメモリのアドレスを取得し、インストゥルメンテーションを行うことができるようになります。
テストケースの追加
src/pkg/runtime/race/testdata/comp_test.go
には、この修正を検証するための新しいテストケースが追加されています。
TestRaceConv1
,TestRaceConv2
,TestRaceConv3
,TestRaceConv4
といったテスト関数が追加されています。- これらのテストは、
P2 P
,S2 S
,X2 X
といった型エイリアスや構造体埋め込み、配列など、様々な型変換とフィールドアクセスの組み合わせを試しています。 - 各テストは、ゴルーチン内でフィールドに書き込みを行い、メインゴルーチンで同じフィールドを読み込むことで、意図的にデータ競合を発生させています。
- この修正が正しく機能していれば、これらのテストは競合検出器によってデータ競合が報告され、テストが成功する(競合が検出される)はずです。
コアとなるコードの変更箇所
src/cmd/gc/racewalk.c
@@ -489,6 +490,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
*np = n;
}
n = treecopy(n);
+ makeaddable(n); // <-- ここで新しくmakeaddableが呼び出される
f = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));
*init = list(*init, f);
return 1;
@@ -496,6 +498,37 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
return 0;
}
+// makeaddable returns a node whose memory location is the
+// same as n, but which is addressable in the Go language
+// sense.
+// This is different from functions like cheapexpr that may make
+// a copy of their argument.
+static void
+makeaddable(Node *n)
+{
+ // The arguments to uintptraddr technically have an address but
+ // may not be addressable in the Go sense: for example, in the case
+ // of T(v).Field where T is a struct type and v is
+ // an addressable value.
+ switch(n->op) {
+ case OINDEX: // 配列/スライス要素アクセス
+ if(isfixedarray(n->left->type))
+ makeaddable(n->left); // 基底配列もアドレス可能にする
+ break;
+ case ODOT: // 構造体フィールドアクセス
+ case OXDOT: // ポインタ経由の構造体フィールドアクセス
+ // Turn T(v).Field into v.Field
+ if(n->left->op == OCONVNOP) // OCONVNOPノードをスキップ
+ n->left = n->left->left;
+ makeaddable(n->left); // フィールドアクセス元もアドレス可能にする
+ break;
+ case ODOTPTR: // ポインタ経由の構造体フィールドアクセス (別ケース)
+ default:
+ // nothing to do
+ break;
+ }
+}
+
static Node*
uintptraddr(Node *n)
{
src/pkg/runtime/race/testdata/comp_test.go
@@ -83,6 +83,60 @@ func TestRaceCompArray(t *testing.T) {
<-c
}
+type P2 P // 型エイリアス
+type S2 S // 型エイリアス
+
+// 型変換後のフィールドアクセス (P(p).x) の競合検出テスト
+func TestRaceConv1(t *testing.T) {
+ c := make(chan bool, 1)
+ var p P2
+ go func() {
+ p.x = 1 // ゴルーチン内で書き込み
+ c <- true
+ }()
+ _ = P(p).x // メインゴルーチンで読み込み (型変換あり)
+ <-c
+}
+
+// ポインタと型変換後のフィールドアクセス (P(*ptr).x) の競合検出テスト
+func TestRaceConv2(t *testing.T) {
+ c := make(chan bool, 1)
+ var p P2
+ go func() {
+ p.x = 1
+ c <- true
+ }()
+ ptr := &p
+ _ = P(*ptr).x // ポインタデリファレンスと型変換あり
+ <-c
+}
+
+// 構造体埋め込みと型変換後のフィールドアクセス (P2(S(s).s1).x) の競合検出テスト
+func TestRaceConv3(t *testing.T) {
+ c := make(chan bool, 1)
+ var s S2
+ go func() {
+ s.s1.x = 1
+ c <- true
+ }()
+ _ = P2(S(s).s1).x // 複数の型変換と構造体フィールドアクセス
+ <-c
+}
+
+type X struct {
+ V [4]P // 配列を含む構造体
+}
+
+type X2 X
+
+// 配列要素と型変換後のフィールドアクセス (P2(X(x).V[1]).x) の競合検出テスト
+func TestRaceConv4(t *testing.T) {
+ c := make(chan bool, 1)
+ var x X2
+ go func() {
+ x.V[1].x = 1 // 配列要素への書き込み
+ c <- true
+ }()
+ _ = P2(X(x).V[1]).x // 配列要素アクセスと型変換
+ <-c
+}
+
type Ptr struct {
s1, s2 *P
}
コアとなるコードの解説
このコミットの主要な変更点は、Goコンパイラのracewalk.c
ファイルにmakeaddable
という新しい静的関数が追加され、既存のcallinstr
関数から呼び出されるようになったことです。
-
makeaddable
関数の目的:- この関数は、競合検出器がインストゥルメンテーションを行う際に、対象となるメモリ位置がGo言語のセマンティクスにおいて「アドレス可能」であることを保証するために導入されました。
- 特に、
T(v).Field
のような式において、T(v)
の部分がコンパイラ内部でOCONVNOP
(何もしない型変換)として表現される場合、その結果がアドレス可能でないと誤って判断される問題がありました。 makeaddable
は、ODOT
やOXDOT
(フィールドアクセス)のノードを処理する際に、もしその左の子ノードがOCONVNOP
であれば、そのOCONVNOP
ノードをスキップして、そのさらに左の子ノード(元の変数v
に相当)を直接参照するようにASTを書き換えます。これにより、T(v).Field
が実質的にv.Field
として扱われ、v
がアドレス可能であれば、そのフィールドもアドレス可能と認識されるようになります。- また、
OINDEX
(配列/スライス要素アクセス)の場合も、基底となる配列が固定長配列であれば、その基底配列もアドレス可能にするために再帰的にmakeaddable
を呼び出します。
-
callinstr
関数からの呼び出し:callinstr
関数は、Goプログラム内のメモリ読み書き操作を特定し、その操作の直前に競合検出器のランタイム関数(raceread
またはracewrite
)への呼び出しを挿入する役割を担っています。- このコミットでは、
uintptraddr(n)
(メモリのアドレスを取得する関数)を呼び出す直前に、makeaddable(n)
が呼び出されるようになりました。これにより、uintptraddr
に渡されるn
が常にアドレス可能であることが保証され、競合検出器が正しくメモリのアドレスを取得し、競合を検出できるようになります。
-
テストケースの追加:
src/pkg/runtime/race/testdata/comp_test.go
に追加された複数のTestRaceConv
関数は、この修正が正しく機能していることを検証するためのものです。- これらのテストは、様々な種類の型変換(例:
P2
からP
への変換)とフィールドアクセスを組み合わせ、意図的にデータ競合を発生させています。 - 修正が適用されていれば、これらのテストは競合検出器によって競合が報告され、テストが成功します。これにより、特定の型変換とフィールドアクセスのパターンにおける競合検出の漏れが解消されたことが確認できます。
この変更により、Goのデータ競合検出器は、より複雑なコードパターン、特に型変換が絡むフィールドアクセスにおいても、データ競合を正確に検出できるようになり、Goプログラムの並行処理の安全性が向上しました。
関連リンク
- Go Issue #5424: https://github.com/golang/go/issues/5424
- Go Code Review: https://golang.org/cl/9033048
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- ThreadSanitizer (TSan) Overview: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppDynamicAnnotations
- Go Language Specification - Selectors: https://go.dev/ref/spec#Selectors
- Go Language Specification - Addressability: https://go.dev/ref/spec#Address_operators (間接的にアドレス可能性について言及)
- Go Compiler Source Code (relevant files like
src/cmd/compile/internal/gc/racewalk.go
in modern Go, though this commit refers tosrc/cmd/gc/racewalk.c
from an older version): https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc - Go AST (Abstract Syntax Tree) Nodes: (Goコンパイラの内部構造に関するドキュメントは公式には少ないが、ソースコードを読むことで理解できる)# [インデックス 16300] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるデータ競合検出器のインストゥルメンテーションに関するバグ修正です。具体的には、T(v).Field
のようなセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を解決します。この修正により、Goのデータ競合検出器がより堅牢になり、特定のコードパターンにおける競合状態を見逃すことがなくなります。
コミット
commit 6c4943cb51cc7c4b27233a7717d74742871f7faa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed May 15 01:25:20 2013 +0200
cmd/gc: fix race instrumentation of selectors T(v).Field
Fixes #5424.
R=golang-dev, daniel.morsing, dvyukov, r
CC=golang-dev
https://golang.org/cl/9033048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6c4943cb51cc7c4b27233a7717d74742871f7faa
元コミット内容
このコミットは、Goコンパイラのcmd/gc
部分におけるデータ競合検出器のインストゥルメンテーションに関する修正です。特に、T(v).Field
という形式のセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を修正します。これは、GoのIssue #5424で報告されたバグに対応するものです。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための組み込みの競合検出器(Race Detector)があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、その検出は並行プログラムのデバッグにおいて非常に重要です。
このコミット以前は、T(v).Field
のような特定のコードパターン、すなわち型変換(T(v)
)の後にその結果の構造体のフィールドにアクセスする(.Field
)場合、競合検出器がそのメモリアクセスを正しくインストゥルメント(計測)できていませんでした。インストゥルメンテーションとは、コンパイラが実行時にメモリアクセスを監視するための追加コードを挿入するプロセスを指します。正しくインストゥルメントされない場合、実際にデータ競合が発生していても検出器がそれを報告しないという問題が生じます。
Issue #5424では、この問題が具体的に報告されており、T(v).Field
のような式が、競合検出器にとって「アドレス可能(addressable)」ではないと誤って判断されることが原因でした。Goの競合検出器は、メモリ位置のアドレスを取得してそのアクセスを記録するため、アドレス可能でない式に対してはインストゥルメンテーションを行うことができません。このコミットは、この特定のエッジケースを修正し、競合検出器の信頼性を向上させることを目的としています。
前提知識の解説
Go言語のデータ競合検出器 (Race Detector)
Goのデータ競合検出器は、Go 1.1で導入された強力なツールです。go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。有効にすると、コンパイラはプログラムのメモリアクセスを監視するための追加コードを挿入します。実行時に、この監視コードが並行アクセスを検出し、データ競合の可能性を報告します。これは、スレッドサニタイザー(ThreadSanitizer, TSan)という技術に基づいています。
cmd/gc
cmd/gc
は、Go言語の公式コンパイラのバックエンド部分を指します。Goのソースコードを機械語に変換する主要な役割を担っています。競合検出器のインストゥルメンテーションは、このコンパイラのフェーズで行われます。具体的には、racewalk.c
のようなファイルが、メモリアクセスを検出して競合検出器のランタイム関数呼び出しを挿入する役割を担っています。
セレクタ (Selectors)
Go言語において、セレクタは構造体のフィールドやメソッドにアクセスするための構文です。例えば、s.Field
やs.Method()
のように使用されます。このコミットで問題となっているのは、T(v).Field
のような形式です。ここでT
は型、v
は変数です。これは、v
を型T
に変換した後に、その結果のフィールドField
にアクセスすることを意味します。
アドレス可能性 (Addressability)
Go言語において、ある式が「アドレス可能」であるとは、その式のメモリ上の位置(アドレス)を取得できることを意味します。アドレス可能である式に対しては、&
演算子を使用してポインタを取得できます。例えば、変数や配列の要素、構造体のフィールドは通常アドレス可能です。しかし、定数、リテラル、関数呼び出しの結果、マップの要素などはアドレス可能ではありません。競合検出器は、メモリアクセスを監視するためにそのアドレスを必要とするため、アドレス可能でない式に対するアクセスは直接インストゥルメントできません。
OCONVNOP
Goコンパイラの内部表現において、OCONVNOP
は「no-op conversion」(何もしない変換)を表すノードタイプです。これは、型変換が行われるが、実行時には実際の値の変更やコピーが発生しないようなケース(例えば、基底型が同じエイリアス型への変換など)で生成されることがあります。このコミットでは、T(v).Field
のようなケースで、T(v)
の部分がOCONVNOP
として表現されることがあり、これが競合検出器のインストゥルメンテーションを妨げていた可能性があります。
技術的詳細
このコミットの核心は、src/cmd/gc/racewalk.c
ファイルに追加されたmakeaddable
関数と、そのcallinstr
関数からの呼び出しです。
racewalk.c
の役割
racewalk.c
は、Goコンパイラのデータ競合検出器のインストゥルメンテーションロジックが含まれるファイルです。このファイル内の関数は、プログラムの抽象構文木(AST)を走査し、メモリ読み書き操作を特定します。そして、これらの操作の直前に、競合検出器のランタイム関数(raceread
やracewrite
)への呼び出しを挿入します。これらのランタイム関数は、アクセスされたメモリのアドレスとサイズを記録し、他のゴルーチンからのアクセスと照合して競合を検出します。
makeaddable
関数の導入
makeaddable
関数は、n
というノード(ASTの要素)を受け取り、そのメモリ位置がn
と同じでありながら、Go言語の意味で「アドレス可能」なノードに変換することを目的としています。これは、cheapexpr
のような関数が引数のコピーを作成する可能性があるのとは異なり、元のメモリ位置を維持しつつアドレス可能性を確保します。
makeaddable
関数は、以下のノードタイプに対して特別な処理を行います。
OINDEX
(配列/スライス要素へのアクセス):- もし
n->left->type
が固定長配列(isfixedarray
)であれば、配列の基底部分(n->left
)もアドレス可能にするために再帰的にmakeaddable(n->left)
を呼び出します。これは、array[index]
のようなアクセスで、array
自体が型変換の結果である場合に重要になります。
- もし
ODOT
(構造体フィールドへのアクセス) およびOXDOT
(ポインタ経由の構造体フィールドへのアクセス):T(v).Field
のようなケースでは、T(v)
の部分がコンパイラ内部でOCONVNOP
ノードとして表現されることがあります。OCONVNOP
は実行時に何もしない変換ですが、競合検出器にとっては「アドレス可能でない」と誤解される原因となっていました。makeaddable
は、n->left->op == OCONVNOP
の場合、n->left
をn->left->left
に直接置き換えます。これにより、T(v)
という変換ノードをスキップし、元の変数v
に直接アクセスする形にASTを書き換えます。これにより、v.Field
という形になり、v
がアドレス可能であれば、そのフィールドもアドレス可能と判断されるようになります。- その後、
makeaddable(n->left)
を再帰的に呼び出し、フィールドアクセス元のノード(変換を剥がした後のv
)もアドレス可能であることを確認します。
ODOTPTR
(ポインタ経由の構造体フィールドへのアクセス):- このケースでは、特に何もする必要がないと判断されています。ポインタ経由のアクセスは通常、既にアドレス可能であるためです。
default
:- 上記以外のノードタイプでは、特に何も処理を行いません。
callinstr
関数での利用
callinstr
関数は、メモリ読み書き操作をインストゥルメントする主要な関数です。この関数内で、uintptraddr(n)
を呼び出す前に、新しく追加されたmakeaddable(n)
が呼び出されます。これにより、uintptraddr
(メモリのアドレスを取得する関数)に渡されるノードn
が、Go言語の意味でアドレス可能であることが保証されます。結果として、T(v).Field
のようなケースでも、競合検出器が正しくメモリのアドレスを取得し、インストゥルメンテーションを行うことができるようになります。
テストケースの追加
src/pkg/runtime/race/testdata/comp_test.go
には、この修正を検証するための新しいテストケースが追加されています。
TestRaceConv1
,TestRaceConv2
,TestRaceConv3
,TestRaceConv4
といったテスト関数が追加されています。- これらのテストは、
P2 P
,S2 S
,X2 X
といった型エイリアスや構造体埋め込み、配列など、様々な型変換とフィールドアクセスの組み合わせを試しています。 - 各テストは、ゴルーチン内でフィールドに書き込みを行い、メインゴルーチンで同じフィールドを読み込むことで、意図的にデータ競合を発生させています。
- この修正が正しく機能していれば、これらのテストは競合検出器によってデータ競合が報告され、テストが成功する(競合が検出される)はずです。
コアとなるコードの変更箇所
src/cmd/gc/racewalk.c
@@ -489,6 +490,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
*np = n;
}
n = treecopy(n);
+ makeaddable(n); // <-- ここで新しくmakeaddableが呼び出される
f = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));
*init = list(*init, f);
return 1;
@@ -496,6 +498,37 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
return 0;
}
+// makeaddable returns a node whose memory location is the
+// same as n, but which is addressable in the Go language
+// sense.
+// This is different from functions like cheapexpr that may make
+// a copy of their argument.
+static void
+makeaddable(Node *n)
+{
+ // The arguments to uintptraddr technically have an address but
+ // may not be addressable in the Go sense: for example, in the case
+ // of T(v).Field where T is a struct type and v is
+ // an addressable value.
+ switch(n->op) {
+ case OINDEX: // 配列/スライス要素アクセス
+ if(isfixedarray(n->left->type))
+ makeaddable(n->left); // 基底配列もアドレス可能にする
+ break;
+ case ODOT: // 構造体フィールドアクセス
+ case OXDOT: // ポインタ経由の構造体フィールドアクセス
+ // Turn T(v).Field into v.Field
+ if(n->left->op == OCONVNOP) // OCONVNOPノードをスキップ
+ n->left = n->left->left;
+ makeaddable(n->left); // フィールドアクセス元もアドレス可能にする
+ break;
+ case ODOTPTR: // ポインタ経由の構造体フィールドアクセス (別ケース)
+ default:
+ // nothing to do
+ break;
+ }
+}
+
static Node*
uintptraddr(Node *n)
{
src/pkg/runtime/race/testdata/comp_test.go
@@ -83,6 +83,60 @@ func TestRaceCompArray(t *testing.T) {
<-c
}
+type P2 P // 型エイリアス
+type S2 S // 型エイリアス
+
+// 型変換後のフィールドアクセス (P(p).x) の競合検出テスト
+func TestRaceConv1(t *testing.T) {
+ c := make(chan bool, 1)
+ var p P2
+ go func() {
+ p.x = 1 // ゴルーチン内で書き込み
+ c <- true
+ }()
+ _ = P(p).x // メインゴルーチンで読み込み (型変換あり)
+ <-c
+}
+
+// ポインタと型変換後のフィールドアクセス (P(*ptr).x) の競合検出テスト
+func TestRaceConv2(t *testing.T) {
+ c := make(chan bool, 1)
+ var p P2
+ go func() {
+ p.x = 1
+ c <- true
+ }()
+ ptr := &p
+ _ = P(*ptr).x // ポインタデリファレンスと型変換あり
+ <-c
+}
+
+// 構造体埋め込みと型変換後のフィールドアクセス (P2(S(s).s1).x) の競合検出テスト
+func TestRaceConv3(t *testing.T) {
+ c := make(chan bool, 1)
+ var s S2
+ go func() {
+ s.s1.x = 1
+ c <- true
+ }()
+ _ = P2(S(s).s1).x // 複数の型変換と構造体フィールドアクセス
+ <-c
+}
+
+type X struct {
+ V [4]P // 配列を含む構造体
+}
+
+type X2 X
+
+// 配列要素と型変換後のフィールドアクセス (P2(X(x).V[1]).x) の競合検出テスト
+func TestRaceConv4(t *testing.T) {
+ c := make(chan bool, 1)
+ var x X2
+ go func() {
+ x.V[1].x = 1 // 配列要素への書き込み
+ c <- true
+ }()
+ _ = P2(X(x).V[1]).x // 配列要素アクセスと型変換
+ <-c
+}
+
type Ptr struct {
s1, s2 *P
}
コアとなるコードの解説
このコミットの主要な変更点は、Goコンパイラのracewalk.c
ファイルにmakeaddable
という新しい静的関数が追加され、既存のcallinstr
関数から呼び出されるようになったことです。
-
makeaddable
関数の目的:- この関数は、競合検出器がインストゥルメンテーションを行う際に、対象となるメモリ位置がGo言語のセマンティクスにおいて「アドレス可能」であることを保証するために導入されました。
- 特に、
T(v).Field
のような式において、T(v)
の部分がコンパイラ内部でOCONVNOP
(何もしない型変換)として表現される場合、その結果がアドレス可能でないと誤って判断される問題がありました。 makeaddable
は、ODOT
やOXDOT
(フィールドアクセス)のノードを処理する際に、もしその左の子ノードがOCONVNOP
であれば、そのOCONVNOP
ノードをスキップして、そのさらに左の子ノード(元の変数v
に相当)を直接参照するようにASTを書き換えます。これにより、T(v).Field
が実質的にv.Field
として扱われ、v
がアドレス可能であれば、そのフィールドもアドレス可能と認識されるようになります。- また、
OINDEX
(配列/スライス要素アクセス)の場合も、基底となる配列が固定長配列であれば、その基底配列もアドレス可能にするために再帰的にmakeaddable
を呼び出します。
-
callinstr
関数からの呼び出し:callinstr
関数は、Goプログラム内のメモリ読み書き操作を特定し、その操作の直前に競合検出器のランタイム関数(raceread
またはracewrite
)への呼び出しを挿入する役割を担っています。- このコミットでは、
uintptraddr(n)
(メモリのアドレスを取得する関数)を呼び出す直前に、makeaddable(n)
が呼び出されるようになりました。これにより、uintptraddr
に渡されるn
が常にアドレス可能であることが保証され、競合検出器が正しくメモリのアドレスを取得し、競合を検出できるようになります。
-
テストケースの追加:
src/pkg/runtime/race/testdata/comp_test.go
に追加された複数のTestRaceConv
関数は、この修正が正しく機能していることを検証するためのものです。- これらのテストは、様々な種類の型変換(例:
P2
からP
への変換)とフィールドアクセスを組み合わせ、意図的にデータ競合を発生させています。 - 修正が適用されていれば、これらのテストは競合検出器によって競合が報告され、テストが成功します。これにより、特定の型変換とフィールドアクセスのパターンにおける競合検出の漏れが解消されたことが確認できます。
この変更により、Goのデータ競合検出器は、より複雑なコードパターン、特に型変換が絡むフィールドアクセスにおいても、データ競合を正確に検出できるようになり、Goプログラムの並行処理の安全性が向上しました。
関連リンク
- Go Issue #5424: https://github.com/golang/go/issues/5424
- Go Code Review: https://golang.org/cl/9033048
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- ThreadSanitizer (TSan) Overview: https://github.com/google/sanitizers/wiki/ThreadSanitizerCppDynamicAnnotations
- Go Language Specification - Selectors: https://go.dev/ref/spec#Selectors
- Go Language Specification - Addressability: https://go.dev/ref/spec#Address_operators (間接的にアドレス可能性について言及)
- Go Compiler Source Code (relevant files like
src/cmd/compile/internal/gc/racewalk.go
in modern Go, though this commit refers tosrc/cmd/gc/racewalk.c
from an older version): https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc - Go AST (Abstract Syntax Tree) Nodes: (Goコンパイラの内部構造に関するドキュメントは公式には少ないが、ソースコードを読むことで理解できる)