[インデックス 14290] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
パッケージ内のレース検出器(race detector)関連のコード、特にracewalk.c
ファイルにおけるいくつかのマイナーな問題を修正するものです。主な目的は、レース検出器の正確性とデバッグ能力を向上させることにあります。具体的には、関数のエントリポイントでのレース検出器フックの挿入順序の修正、デバッグ情報の強化、型スイッチ式における潜在的なメモリアクセスの考慮、そしてブランク識別子(_
)の扱いに関する修正が含まれています。
コミット
commit de10a23db14106588b608d724510849c4e5f278a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Nov 1 22:56:04 2012 +0400
cmd/gc: racewalk: fix a bunch of minor issues
1. Prepend racefuncenter() to fn->enter -- fn->enter can contain new() calls,
and we want them to be in the scope of the function.
2. Dump fn->enter and fn->exit.
3. Add TODO that OTYPESW expression can contain interesting memory accesses.
4. Ignore only _ names instead of all names starting with _.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6822048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/de10a23db14106588b608d724510849c4e5f278a
元コミット内容
cmd/gc: racewalk: fix a bunch of minor issues
1. Prepend racefuncenter() to fn->enter -- fn->enter can contain new() calls,
and we want them to be in the scope of the function.
2. Dump fn->enter and fn->exit.
3. Add TODO that OTYPESW expression can contain interesting memory accesses.
4. Ignore only _ names instead of all names starting with _.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6822048
変更の背景
このコミットは、Goのレース検出器(race detector)の内部実装におけるいくつかの細かな不具合と改善点を修正するために行われました。レース検出器は、並行プログラムにおけるデータ競合(data race)を検出するための重要なツールであり、その正確性と効率性はGoプログラムの信頼性を保証する上で不可欠です。
具体的な背景としては、以下の点が挙げられます。
racefuncenter
の挿入タイミング:racefuncenter
は、関数のエントリ時に呼び出されるレース検出器のフック関数です。このフックが関数の初期化コード(特にnew()
のようなメモリ割り当てを行う呼び出し)よりも後に挿入されると、これらの初期化処理がレース検出器の監視範囲外になってしまう可能性がありました。これを修正し、関数のスコープ全体でレース検出器が有効になるように、racefuncenter
の呼び出しをfn->enter
リストの先頭に移動する必要がありました。- デバッグ情報の強化: コンパイラのデバッグ時には、関数のエントリ(
fn->enter
)とエグジット(fn->exit
)のコードリストの内容を確認できると、問題の特定が容易になります。このコミットでは、デバッグフラグが有効な場合にこれらのリストの内容をダンプする機能が追加されました。 OTYPESW
式の考慮:OTYPESW
はGoの型スイッチ(type switch)を表す内部ノードです。型スイッチの式自体が、関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があります。以前の実装では、この可能性が十分に考慮されていなかったため、将来的な改善点としてTODOコメントが追加されました。- ブランク識別子(
_
)の厳密な無視: Go言語では、ブランク識別子_
は値を破棄するために使用されます。レース検出器がコードを解析する際、この_
に関連するシンボルは通常無視されるべきです。しかし、以前の実装では、_
で始まるすべての名前(例:_myVar
)を無視してしまっていました。これは意図しない副作用を引き起こす可能性があったため、厳密に_
という名前のみを無視するように修正されました。
これらの修正は、レース検出器の堅牢性を高め、より正確なデータ競合の検出を可能にすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラおよびレース検出器に関する前提知識が必要です。
- Goコンパイラ(
cmd/gc
): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。cmd/gc
は、フロントエンド(構文解析、型チェック)、ミドルエンド(最適化)、バックエンド(コード生成)の各フェーズを持ちます。 - レース検出器(Race Detector): Go言語に組み込まれているツールで、並行プログラムにおけるデータ競合(data race)を検出します。データ競合とは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。レース検出器は、コンパイル時または実行時にコードにフックを挿入することで、メモリアクセスを監視し、競合を報告します。
racewalk.c
: Goコンパイラのcmd/gc
ディレクトリにあるC言語のソースファイルで、レース検出器の主要なロジックが実装されています。このファイルは、Goの抽象構文木(AST)を走査し、レース検出に必要なインストゥルメンテーション(フックの挿入)を行います。- 抽象構文木(AST)と
Node
、NodeList
: コンパイラはソースコードを解析して抽象構文木(AST)を構築します。ASTはプログラムの構造を木構造で表現したものです。Node
はASTの各ノード(式、文、宣言など)を表すデータ構造であり、NodeList
はNode
のリストです。 fn->enter
とfn->exit
: Goコンパイラの内部では、各関数(fn
)は、その関数のエントリ時に実行されるべきコードのリスト(fn->enter
)と、エグジット時に実行されるべきコードのリスト(fn->exit
)を持つことができます。レース検出器は、これらのリストにracefuncenter
やracefuncexit
といったフック関数を挿入します。mkcall
: Goコンパイラの内部関数で、指定された関数を呼び出すためのASTノード(Node
)を作成します。OTYPESW
: Goコンパイラの内部で定義されているASTノードのオペレーションコードの一つで、型スイッチ(switch v.(type)
)を表します。ONAME
: ASTノードのオペレーションコードの一つで、名前(変数、関数名など)を表します。TSTRUCT
: 型を表す内部構造体で、特に構造体型を示します。sym
: シンボル(symbol)を表す内部構造体で、変数名、関数名などの識別子に関連付けられた情報を含みます。- ブランク識別子(
_
): Go言語の特殊な識別子で、値を明示的に破棄するために使用されます。例えば、_ = someFunc()
のように、戻り値を無視する場合に用いられます。
技術的詳細
このコミットで行われた技術的な変更は、以下の4つの主要な点に集約されます。
-
racefuncenter()
のfn->enter
への挿入順序の変更:- 変更前:
racefuncenter
の呼び出しは、fn->enter
リストの末尾に追加されていました(fn->enter = list(fn->enter, nd);
)。 - 問題点:
fn->enter
には、関数の初期化処理(例:new()
によるメモリ割り当て)が含まれる可能性がありました。racefuncenter
がこれらの処理の後に挿入されると、初期化処理中のメモリアクセスがレース検出器の監視対象外となるリスクがありました。 - 変更後:
racefuncenter
の呼び出しを表すノードnd
が、fn->enter
リストの先頭に挿入されるように変更されました(fn->enter = concat(list1(nd), fn->enter);
)。これにより、関数のエントリ時に実行されるすべてのコードがracefuncenter
のスコープ内で実行されることが保証され、レース検出の正確性が向上します。 - コードの差分:
(注: 元のdiffにはマージコンフリクトマーカーが含まれていましたが、最終的な変更は上記のように- fn->enter = list(fn->enter, nd); + nd = mkcall("racefuncenter", T, nil); + fn->enter = concat(list1(nd), fn->enter);
concat
による先頭への追加です。)
- 変更前:
-
fn->enter
とfn->exit
のデバッグダンプの追加:- 変更前: デバッグモード(
debug['W']
が有効な場合)では、関数のボディ(curfn->nbody
)のみがダンプされていました。 - 変更後:
racewalk
関数の最後に、fn->enter
とfn->exit
の内容もデバッグ出力としてダンプされるように変更されました。これにより、レース検出器が挿入したフックが正しく配置されているかを確認しやすくなり、デバッグ作業が効率化されます。 - コードの差分:
--- a/src/cmd/gc/racewalk.c +++ b/src/cmd/gc/racewalk.c @@ -59,6 +59,9 @@ racewalk(Node *fn) \tif(debug['W']) { \t\tsnprint(s, sizeof(s), "after racewalk %S", curfn->nname->sym); \t\tdumplist(s, curfn->nbody); +\t\tsnprint(s, sizeof(s), "after walk %S", curfn->nname->sym); +\t\tdumplist(s, curfn->nbody); +\t\tsnprint(s, sizeof(s), "enter %S", curfn->nname->sym); +\t\tdumplist(s, curfn->enter); \t} }
- 変更前: デバッグモード(
-
OTYPESW
式に関するTODOコメントの追加:- 変更前:
OSWITCH
(スイッチ文)のOTYPESW
(型スイッチ)ケースでは、「静的型付けがあるため、気にする必要はない」というコメントがありました。 - 問題点: 型スイッチの式自体が、関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があります。この可能性が以前は十分に考慮されていませんでした。
- 変更後: 既存のコメントが削除され、代わりに「式が呼び出しや読み込みを含む可能性がある」というTODOコメントが追加されました。これは、将来的にこのケースでのレース検出器の処理を改善する必要があることを示唆しています。
- コードの差分:
--- a/src/cmd/gc/racewalk.c +++ b/src/cmd/gc/racewalk.c @@ -163,7 +175,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip) \n \tcase OSWITCH: \t\tif(n->ntest->op == OTYPESW) -\t\t\t// don't bother, we have static typization +\t\t\t// TODO(dvyukov): the expression can contain calls or reads. \t\t\treturn; \t\tracewalknode(&n->ntest, &n->ninit, 0, 0); \t\tracewalklist(n->nbody, nil);
- 変更前:
-
ブランク識別子(
_
)の無視ロジックの厳密化:- 変更前:
callinstr
関数内で、シンボル名が_
で始まる場合にそのシンボルを無視していました(strncmp(n->sym->name, "_", sizeof("_")-1) == 0
)。これは、_myVar
のような名前も無視してしまう可能性がありました。 - 問題点: Go言語では、ブランク識別子
_
は特定の目的(値の破棄など)のために使用され、それ以外の_
で始まる名前は通常の識別子として扱われるべきです。strncmp
を使用すると、_
で始まるすべての名前がブランク識別子として誤って扱われる可能性がありました。 - 変更後:
strncmp
がstrcmp
に変更され、シンボル名が厳密に_
である場合にのみ無視されるようになりました(strcmp(n->sym->name, "_") == 0
)。これにより、ブランク識別子の扱いがより正確になり、意図しないシンボルの無視が防止されます。同様の修正が、構造体フィールドの走査ロジックにも適用されています。 - コードの差分:
--- a/src/cmd/gc/racewalk.c +++ b/src/cmd/gc/racewalk.c @@ -369,12 +381,12 @@ callinstr(Node *n, NodeList **init, int wr, int skip) \tif(n->op == ONAME) { \t\tif(n->sym != S) { \t\t\tif(n->sym->name != nil) { -\t\t\t\tif(strncmp(n->sym->name, "_", sizeof("_")-1) == 0) +\t\t\t\tif(strcmp(n->sym->name, "_") == 0) \t\t\t\t\treturn 0; \t\t\t\tif(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0) \t\t\t\t\treturn 0; \t\t\t\t... \t\t\t\t... \t\t\t\tfor(t1=t->type; t1; t1=t1->down) { -\t\t\t\tif(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) { +\t\t\t\tif(t1->sym && strcmp(t1->sym->name, "_")) { \t\t\t\t\tn = treecopy(n); \t\t\t\t\tf = nod(OXDOT, n, newname(t1->sym)); \t\t\t\t\tif(callinstr(f, init, wr, 0)) {
- 変更前:
コアとなるコードの変更箇所
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -43,19 +43,24 @@ racewalk(Node *fn)
}
}
-<<<<<<< local
- // TODO(dvyukov): ideally this should be:
- // racefuncenter(getreturnaddress())
- // because it's much more costly to obtain from runtime library.
- nd = mkcall("racefuncenter", T, nil);
- fn->enter = concat(list1(nd), fn->enter);
-=======
- // nodpc is the PC of the caller as extracted by
- // getcallerpc. We use -widthptr(FP) for x86.
- // BUG: this will not work on arm.
- // TODO(rsc): this is wrong. getcallerpc is not a function.
- // It's a pseudo-function that the compiler rewrites.
- nodpc = nod(ONAME, sysvar("FP"), nil);
- nodpc->xoffset = -widthptr;
- nd = mkcall("racefuncenter", T, nil, nodpc);
- fn->enter = list(fn->enter, nd);
->>>>>>> other
+ // TODO(dvyukov): ideally this should be:
+ // racefuncenter(getreturnaddress())
+ // because it's much more costly to obtain from runtime library.
+ nd = mkcall("racefuncenter", T, nil);
+ fn->enter = concat(list1(nd), fn->enter);
nd = mkcall("racefuncexit", T, nil);
- fn->exit = list(fn->exit, nd); // works fine if (!fn->exit)
+ fn->exit = list(fn->exit, nd);
racewalklist(curfn->nbody, nil);
if(debug['W']) {
snprint(s, sizeof(s), "after racewalk %S", curfn->nname->sym);
dumplist(s, curfn->nbody);
+ snprint(s, sizeof(s), "after walk %S", curfn->nname->sym);
+ dumplist(s, curfn->nbody);
+ snprint(s, sizeof(s), "enter %S", curfn->nname->sym);
+ dumplist(s, curfn->enter);
}
}
@@ -163,7 +168,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
case OSWITCH:
if(n->ntest->op == OTYPESW)
- // don't bother, we have static typization
+ // TODO(dvyukov): the expression can contain calls or reads.
return;
racewalknode(&n->ntest, &n->ninit, 0, 0);
racewalklist(n->nbody, nil);
@@ -369,12 +374,12 @@ callinstr(Node *n, NodeList **init, int wr, int skip)
if(n->op == ONAME) {
if(n->sym != S) {
if(n->sym->name != nil) {
- if(strncmp(n->sym->name, "_", sizeof("_")-1) == 0)
+ if(strcmp(n->sym->name, "_") == 0)
return 0;
if(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0)
return 0;
...
...
for(t1=t->type; t1; t1=t1->down) {
- if(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) {
+ if(t1->sym && strcmp(t1->sym->name, "_")) {
n = treecopy(n);
f = nod(OXDOT, n, newname(t1->sym));
if(callinstr(f, init, wr, 0)) {
コアとなるコードの解説
上記のコード変更は、src/cmd/gc/racewalk.c
ファイル内のracewalk
関数とcallinstr
関数に集中しています。
-
racewalk
関数内の変更(racefuncenter
の挿入とデバッグダンプ):fn->enter = concat(list1(nd), fn->enter);
- これは、
racefuncenter
の呼び出しを表すノードnd
を、関数のエントリ時に実行されるコードのリストfn->enter
の先頭に挿入する行です。list1(nd)
はnd
のみを含む新しいリストを作成し、concat
はそのリストと既存のfn->enter
リストを結合します。これにより、関数内の他の初期化処理(例:new()
呼び出し)よりも先にracefuncenter
が実行されることが保証され、レース検出の範囲が関数の最初からカバーされるようになります。
- これは、
snprint(s, sizeof(s), "enter %S", curfn->nname->sym);
dumplist(s, curfn->enter);
- これらの行は、デバッグフラグ
debug['W']
が有効な場合に、現在の関数(curfn
)のエントリコードリストcurfn->enter
の内容をダンプするためのものです。これにより、コンパイラの開発者は、レース検出器が挿入したフックが正しく配置されているか、またfn->enter
リストにどのようなコードが含まれているかを視覚的に確認できるようになります。
- これらの行は、デバッグフラグ
-
racewalknode
関数内の変更(OTYPESW
のTODOコメント):// TODO(dvyukov): the expression can contain calls or reads.
- これは、
OSWITCH
(スイッチ文)のOTYPESW
(型スイッチ)ケースにおけるコメントの変更です。以前のコメントは「静的型付けがあるため、気にする必要はない」というものでしたが、型スイッチの式自体が関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があるため、その点を考慮する必要があるというTODOコメントに置き換えられました。これは、将来的な改善の必要性を示しています。
- これは、
-
callinstr
関数内の変更(ブランク識別子の厳密化):if(strcmp(n->sym->name, "_") == 0)
- この行は、
n->sym->name
(シンボル名)が厳密に_
(ブランク識別子)であるかどうかをstrcmp
関数で比較しています。以前はstrncmp
が使用されており、_
で始まるすべての名前を無視してしまう可能性がありましたが、strcmp
を使用することで、正確にブランク識別子のみを無視するようになります。これは、レース検出器が不要なシンボルを追跡しないようにするための最適化であり、同時に_
で始まる他の有効な識別子を誤って無視しないようにするための修正です。
- この行は、
- 同様の修正が、構造体フィールドを走査するループ内の
if(t1->sym && strcmp(t1->sym->name, "_"))
にも適用されています。
これらの変更は、Goのレース検出器の内部動作をより正確かつ堅牢にし、デバッグを容易にすることを目的としています。
関連リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
- Go CL 6822048: https://golang.org/cl/6822048 (元のGerritレビューページ)
参考にした情報源リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- Go Compiler Source Code (GitHub): https://github.com/golang/go
cmd/gc/racewalk.c
in Go source: https://github.com/golang/go/blob/master/src/cmd/gc/racewalk.cstrcmp
andstrncmp
C functions documentation (general knowledge)- Go Blank Identifier: https://go.dev/doc/effective_go#blank
- Go AST (Abstract Syntax Tree) concepts (general compiler knowledge)
- Go Compiler Internals (various online resources and talks about Go compiler architecture)
- (Specific links for compiler internals are hard to pinpoint for general knowledge, but understanding how Go's
gc
works is crucial.) - A good starting point for Go compiler internals: https://go.dev/blog/go1.5-compiler (though this commit predates Go 1.5, the general concepts apply)
- Go's internal representation (Nodes, Opcodes): https://go.dev/src/cmd/compile/internal/syntax/nodes.go (for modern Go, but concepts are similar)
- Go's
cmd/compile
directory structure: https://github.com/golang/go/tree/master/src/cmd/compile
- (Specific links for compiler internals are hard to pinpoint for general knowledge, but understanding how Go's