[インデックス 14402] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)のracewalk
ツールにおける構造体の計測に関する修正です。特に、go.itab
に関連する人工的な構造体やコンパイラが生成する一時変数など、データ競合が発生し得ない箇所での計測をスキップするように変更されています。これにより、racewalk
の効率性と正確性が向上します。
コミット
commit 96833d3a25078aae1b5e279775bebb09a054b4c8
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Nov 14 16:26:00 2012 +0400
cmd/gc: racewalk: fix instrumentation of structs
+ do not instrument go.itab.*
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6819106
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/96833d3a25078aae1b5e279775bebb09a054b4c8
元コミット内容
cmd/gc: racewalk: fix instrumentation of structs
+ do not instrument go.itab.*
変更の背景
Go言語のコンパイラには、データ競合(data race)を検出するためのracewalk
というツールが存在します。このツールは、プログラムの実行中にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合にデータ競合を報告します。この監視を行うために、racewalk
はコンパイル時にコードに「計測(instrumentation)」と呼ばれる追加のコードを挿入します。
しかし、すべてのメモリアクセスを計測する必要はありません。特に、コンパイラが内部的に生成する一時変数や、Goランタイムが管理する特定の構造体(例: go.itab
)は、通常のユーザーコードからは直接アクセスされず、データ競合が発生する可能性が極めて低いか、あるいは発生しても問題とならない性質を持っています。これらの「人工的な」要素まで計測してしまうと、不必要なオーバーヘッドが発生し、racewalk
のパフォーマンスが低下したり、誤検知につながる可能性がありました。
このコミットは、このような不必要な計測を排除し、racewalk
の効率性と正確性を向上させることを目的としています。
前提知識の解説
- データ競合 (Data Race): 複数の並行実行される処理(Goではゴルーチン)が、同期メカニズムなしに同じメモリ位置に同時にアクセスし、そのうち少なくとも1つが書き込み操作である場合に発生するプログラミング上のバグです。データ競合は予測不能な動作やクラッシュを引き起こす可能性があります。
racewalk
: Goコンパイラ(cmd/gc
)の一部として実装されている、データ競合検出のための静的解析およびコード計測ツールです。コンパイル時にコードにフックを挿入し、実行時にメモリアクセスを監視します。- 計測 (Instrumentation): プログラムの実行時の振る舞いを監視するために、追加のコード(プローブやフック)を元のプログラムに挿入するプロセスです。これにより、パフォーマンス、カバレッジ、またはデータ競合などの問題を分析できます。
cmd/gc
: Go言語の公式コンパイラです。Goソースコードを機械語に変換する役割を担います。go.itab
: Goランタイム内部で使用されるインターフェーステーブル(interface table)の略です。Goのインターフェースは、動的なディスパッチを可能にするために、型情報とメソッドセットを保持するitab
という構造体を参照します。itab
はコンパイラとランタイムによって内部的に管理され、ユーザーコードが直接操作することはありません。ONAME
: GoコンパイラのAST(抽象構文木)におけるノードの種類の一つで、名前付きのエンティティ(変数、関数など)を表します。autotmp_
/statictmp_
: Goコンパイラが内部的に生成する一時変数や静的変数のプレフィックスです。これらはユーザーコードからは直接参照されず、コンパイラが最適化やコード生成のために使用します。ODOT
,OXDOT
,OCONVNOP
,OCONV
,OPAREN
: これらはGoコンパイラのASTにおける演算子ノードの種類です。ODOT
,OXDOT
: 構造体のフィールドアクセス(例:s.field
)を表します。OCONVNOP
,OCONV
: 型変換(キャスト)を表します。OPAREN
: 括弧(例:(expr)
)を表します。
技術的詳細
このコミットの主要な変更点は、src/cmd/gc/racewalk.c
ファイルにisartificial
という新しい静的関数が導入されたことです。この関数は、与えられたNode
(ASTのノード)が、データ競合の計測対象から除外すべき「人工的な」要素であるかどうかを判定します。
isartificial
関数は以下の条件をチェックします。
ONAME
ノードであること: ノードが名前付きのエンティティを表す場合。- シンボル名による判定:
_
(ブランク識別子): 書き込みが行われても値が破棄されるため、競合の対象外。autotmp_
で始まる名前: コンパイラが生成するローカルな一時変数であり、通常は単一のゴルーチンによってのみアクセスされるため、競合の対象外。statictmp_
で始まる名前: コンパイラが生成する静的な一時変数であり、通常は読み取り専用であるか、コンパイラによって安全に管理されるため、競合の対象外。go.itab
パッケージ内のシンボル:go.itab
はランタイムが管理する内部構造体であり、ユーザーコードからの直接的な競合は発生しないと仮定されます。
callinstr
関数は、コードに計測を挿入するかどうかを決定する主要な関数です。このコミットでは、callinstr
の冒頭でisartificial(n)
を呼び出し、もしノードが人工的なものであれば即座に計測をスキップするように変更されました。
また、構造体のフィールドアクセスを処理するcallinstr
内のロジックも修正されています。特に、OXDOT
ノード(フィールドアクセス)がtreecopy
された際に、そのtype
フィールドが正しく設定されるようにf->type = t1;
が追加されました。これは、計測対象のノードの型情報が正しく伝播されることを保証します。
さらに、basenod
関数も更新され、ODOT
だけでなくOXDOT
, OCONVNOP
, OCONV
, OPAREN
といったノードタイプも、ベースとなるノードを見つける際にスキップされるようになりました。これにより、より複雑な式の中にあるベースノード(例えば、(*ptr).field
のようなケース)も正しく識別できるようになり、計測の適用範囲がより正確になります。
コアとなるコードの変更箇所
src/cmd/gc/racewalk.c
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -382,6 +382,27 @@ ret:
*np = n;
}
+static int
+isartificial(Node *n)
+{
+ // compiler-emitted artificial things that we do not want to instrument,
+ // cant' possibly participate in a data race.
+ if(n->op == ONAME && n->sym != S && n->sym->name != nil) {
+ if(strcmp(n->sym->name, "_") == 0)
+ return 1;
+ // autotmp's are always local
+ if(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0)
+ return 1;
+ // statictmp's are read-only
+ if(strncmp(n->sym->name, "statictmp_", sizeof("statictmp_")-1) == 0)
+ return 1;
+ // go.itab is accessed only by the compiler and runtime (assume safe)
+ if(n->sym->pkg && n->sym->pkg->name && strcmp(n->sym->pkg->name, "go.itab") == 0)
+ return 1;
+ }
+ return 0;
+}
+
static int
callinstr(Node **np, NodeList **init, int wr, int skip)
{
@@ -390,25 +411,18 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
int class, res, hascalls;
n = *np;
- //print("callinstr for %N [ %s ] etype=%d class=%d\n",
- // n, opnames[n->op], n->type ? n->type->etype : -1, n->class);
+ //print("callinstr for %+N [ %O ] etype=%d class=%d\n",
+ // n, n->op, n->type ? n->type->etype : -1, n->class);
if(skip || n->type == T || n->type->etype >= TIDEAL)
return 0;
t = n->type;
- if(n->op == ONAME) {
- if(n->sym != S) {
- if(n->sym->name != nil) {
- if(strcmp(n->sym->name, "_") == 0)
- return 0;
- if(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0)
- return 0;
- if(strncmp(n->sym->name, "statictmp_", sizeof("statictmp_")-1) == 0)
- return 0;
- }
- }
- }
+ if(isartificial(n))
+ return 0;
if(t->etype == TSTRUCT) {
+ // PARAMs w/o PHEAP are not interesting.
+ if(n->class == PPARAM || n->class == PPARAMOUT)
+ return 0;
\tres = 0;
\thascalls = 0;
\tforeach(n, hascallspred, &hascalls);
@@ -420,6 +434,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
if(t1->sym && strcmp(t1->sym->name, "_")) {
n = treecopy(n);
f = nod(OXDOT, n, newname(t1->sym));
+ f->type = t1;
if(callinstr(&f, init, wr, 0)) {
typecheck(&f, Erv);
res = 1;
@@ -430,6 +445,9 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
}
b = basenod(n);
+ // it skips e.g. stores to ... parameter array
+ if(isartificial(b))
+ return 0;
class = b->class;
// BUG: we _may_ want to instrument PAUTO sometimes
// e.g. if we've got a local variable/method receiver
@@ -467,7 +485,7 @@ static Node*
basenod(Node *n)
{
for(;;) {
-\t\tif(n->op == ODOT || n->op == OPAREN) {
+\t\tif(n->op == ODOT || n->op == OXDOT || n->op == OCONVNOP || n->op == OCONV || n->op == OPAREN) {
\t\t\tn = n->left;
\t\t\tcontinue;
\t\t}
コアとなるコードの解説
-
isartificial
関数の追加:- この新しい関数は、与えられたASTノード
n
が、データ競合の計測から除外すべき人工的な要素であるかを判定します。 ONAME
ノード(名前付きのエンティティ)の場合に、そのシンボル名が_
、autotmp_
で始まる、statictmp_
で始まる、またはgo.itab
パッケージに属するかどうかをチェックします。- これらの条件に合致する場合、
1
(true)を返し、計測をスキップするよう指示します。
- この新しい関数は、与えられたASTノード
-
callinstr
関数でのisartificial
の利用:callinstr
関数の冒頭で、if(isartificial(n))
というチェックが追加されました。- これにより、人工的なノードが計測対象として渡された場合、それ以降の複雑な計測ロジックを実行することなく、早期に
0
(計測不要)を返すことで処理を終了します。これはパフォーマンスの向上に寄与します。 - 以前は
callinstr
内に直接埋め込まれていた_
,autotmp_
,statictmp_
のチェックがisartificial
に集約され、コードの可読性と保守性が向上しました。
-
構造体計測ロジックの改善:
if(t->etype == TSTRUCT)
ブロック内で、PARAM
(関数パラメータ)やPARAMOUT
(戻り値パラメータ)で、かつPHEAP
(ヒープに割り当てられていない)でないものは計測しないという条件が追加されました。これは、スタック上のパラメータは通常、単一のゴルーチンによって安全にアクセスされるため、競合の対象外と見なせるためです。f->type = t1;
の追加: 構造体のフィールドアクセス(OXDOT
)を処理する際に、新しく作成されたノードf
の型情報が正しく設定されるようになりました。これにより、後続の型チェックや計測処理が正確に行われます。
-
basenod
関数の拡張:basenod
関数は、複雑な式の中から「ベース」となるノード(例えば、a.b.c
におけるa
)を見つけるために使用されます。- 変更前は
ODOT
とOPAREN
のみをスキップしていましたが、このコミットではOXDOT
,OCONVNOP
,OCONV
もスキップするようになりました。これにより、型変換やその他の演算子を含む式でも、より正確にベースノードを特定できるようになり、計測の適用範囲が適切になります。
これらの変更により、racewalk
はデータ競合が発生し得ないコンパイラ内部の要素や一時変数に対する不必要な計測を避け、より効率的かつ正確にデータ競合を検出できるようになりました。
関連リンク
- Go言語のデータ競合検出: https://go.dev/doc/articles/race_detector
- Goコンパイラのソースコード: https://github.com/golang/go/tree/master/src/cmd/compile
参考にした情報源リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
- Go Compiler Internals (General): https://go.dev/blog/go-compiler-internals
- Go
itab
(Interface Table) explanation: https://go.dev/blog/go-interface-values (間接的にitab
について触れられています) - Go AST (Abstract Syntax Tree) concepts: https://go.dev/blog/go-ast
- Go
cmd/gc
source code: https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc (当時のパスはsrc/cmd/gc
でしたが、現在はsrc/cmd/compile/internal/gc
に移動しています) - Dmitriy Vyukov's contributions to Go: (一般的な検索結果から、彼がGoの並行処理とツールに大きく貢献していることがわかります)
golang.org/cl/6819106
: https://golang.org/cl/6819106 (GoのコードレビューシステムGerritの変更リスト)