[インデックス 14309] ファイルの概要
このコミットは、Go言語のコンパイラ(cmd/gc
)におけるレース検出器の計装(instrumentation)に関する修正です。具体的には、BLOCK
ノードの処理方法を改善し、関数呼び出しやインライン化された関数に関連するブロックが適切にレース検出の対象となるように変更しています。
コミット
commit c46f1f40daad83c940e1b3c09f77b9867d598473
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Sat Nov 3 00:11:06 2012 +0100
cmd/gc: instrument blocks for race detection.
It happens that blocks are used for function calls in a
quite low-level way so they cannot be instrumented as
usual.
Blocks are also used for inlined functions.
R=golang-dev, rsc, dvyukov
CC=golang-dev
https://golang.org/cl/6821068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c46f1f40daad83c940e1b3c09f77b9867d598473
元コミット内容
cmd/gc: instrument blocks for race detection.
このコミットは、Goコンパイラ(cmd/gc
)において、レース検出のためにブロック(BLOCK
ノード)を計装するものです。
コミットメッセージによると、ブロックは関数呼び出しのために低レベルで使用されるため、通常の方法では計装できない問題がありました。また、インライン化された関数にもブロックが使用されることが指摘されています。
変更の背景
Go言語には、並行処理におけるデータ競合(data race)を検出するための組み込みのレース検出器(Race Detector)があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。これはプログラムの予測不能な動作やクラッシュを引き起こす可能性があります。
レース検出器は、コンパイル時または実行時にコードに特殊な計装(instrumentation)を挿入することで機能します。この計装は、メモリへのアクセス(読み書き)を監視し、競合のパターンを検出します。
このコミットの背景には、Goコンパイラ(cmd/gc
)が生成する中間表現(IR)において、BLOCK
ノードが特定の状況(特に複数の戻り値を持つ関数呼び出しやインライン化された関数)で特殊な役割を果たしており、これらのブロックがレース検出器によって適切に計装されていなかったという問題がありました。
従来の処理では、OBLOCK
ノードに遭遇すると「leads to crashes. //racewalklist(n->list, nil);」というコメントと共に処理がスキップされており、これがレース検出の漏れにつながっていたと考えられます。このコミットは、この問題を解決し、より網羅的なレース検出を可能にすることを目的としています。
前提知識の解説
Go言語のコンパイラ (cmd/gc
)
cmd/gc
は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成など、複数のフェーズがあります。このコミットが関連するracewalk.c
は、中間コードの走査とレース検出器のための計装を行う部分です。
Go言語のレース検出器 (Race Detector)
Goのレース検出器は、実行時にデータ競合を検出するツールです。go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。これは、プログラムの実行中にメモリへのアクセスを監視し、競合が発生した可能性のある場所を報告します。内部的には、ThreadSanitizer (TSan) の技術をベースにしています。TSanは、メモリへのアクセス履歴を記録し、並行アクセスパターンを分析することでデータ競合を検出します。
抽象構文木 (AST) と中間表現 (IR)
コンパイラは、ソースコードを直接機械語に変換するのではなく、いくつかの抽象的な表現に変換しながら処理を進めます。
- 抽象構文木 (AST): ソースコードの構文構造を木構造で表現したものです。各ノードは、式、文、宣言などを表します。
- 中間表現 (IR): ASTよりも低レベルで、機械語に近いが、特定のCPUアーキテクチャに依存しない形式です。コンパイラの最適化フェーズで利用されます。Goコンパイラでは、ASTからIRへの変換が行われ、このIRに対して様々な最適化や計装が施されます。
BLOCK
ノード (OBLOCK
)
Goコンパイラの中間表現において、BLOCK
ノード(OBLOCK
)は、複数のステートメントをまとめるために使用される構造です。これは、C言語の{ ... }
ブロックや、複数の文を順次実行するシーケンスに似ています。
コミットメッセージが示唆するように、BLOCK
ノードは単なるコードブロックだけでなく、以下のような低レベルな用途にも使われます。
- 複数の戻り値を持つ関数呼び出し: Goでは、関数が複数の値を返すことができます。例えば、
x, y := f()
のような呼び出しは、コンパイラ内部ではBLOCK
ノードとして表現されることがあります。このブロック内には、関数呼び出し自体と、戻り値をそれぞれの変数に割り当てる操作が含まれます。 - インライン化された関数: コンパイラ最適化の一環として、小さな関数は呼び出し元に直接展開(インライン化)されることがあります。インライン化された関数の本体も、
BLOCK
ノードとして表現されることがあります。 - ループの初期化:
for
ループなどの初期化部分もBLOCK
ノードで表現されることがあります。
racewalknode
と racewalklist
racewalknode
とracewalklist
は、src/cmd/gc/racewalk.c
ファイル内で定義されている関数で、Goコンパイラのレース検出器の計装フェーズにおいて、AST/IRノードを走査(walk)し、必要な計装コードを挿入する役割を担っています。
racewalknode
: 単一のノードを走査します。racewalklist
: ノードのリストを走査します。
技術的詳細
このコミットの核心は、racewalk.c
内のracewalknode
関数におけるOBLOCK
ノードの処理ロジックの変更です。
変更前は、OBLOCK
ノードに遭遇すると、その内部のリスト(n->list
)の走査がコメントアウトされており、クラッシュを引き起こす可能性があるためスキップされていました。これは、OBLOCK
が特定の低レベルな構造(特に複数の戻り値を持つ関数呼び出し)で使用される際に、単純な走査と計装が問題を引き起こすことを示唆しています。例えば、x, y := f()
のようなコードがBLOCK{CALL f, AS x [SP+0], AS y [SP+n]}
のように表現される場合、CALL f
とAS x
、AS y
の間にレース検出の計装を挿入すると、関数の戻り値がスタックに配置される前に計装コードが実行され、結果が破壊される("smash the results")可能性がありました。
このコミットでは、OBLOCK
ノードの処理をより詳細に制御するために、n->list->n->op
(ブロック内の最初のステートメントの操作)に基づいてswitch
文を導入しています。
-
関数呼び出しのブロック (
OCALLFUNC
,OCALLMETH
,OCALLINTER
):- これらのケースでは、ブロックが複数の戻り値を持つ関数呼び出しのために使用されていると判断されます。
racewalknode(&n->list->n, &n->ninit, 0, 0);
を呼び出すことで、関数呼び出し自体(n->list->n
)のみを計装します。この際、wr
(書き込みフラグ)は0
(読み込み)に設定され、skip
も0
です。fini = nil; racewalklist(n->list->next, &fini);
を呼び出すことで、**関数呼び出しに続くステートメント(戻り値の割り当てなど)**を計装します。この際、fini
リストに計装結果が格納されます。n->list = concat(n->list, fini);
によって、元のブロックリストと、後続のステートメントの計装結果を結合します。- このアプローチにより、関数呼び出しと戻り値の割り当ての間に計装コードが挿入されることを避け、結果が破壊されるのを防ぎつつ、関数呼び出し自体はレース検出の対象とすることができます。
-
その他の通常のブロック (
default
):- 上記以外の
OBLOCK
ノード(例: ループの初期化、インライン化された関数の本体など)は、通常のブロックとして扱われます。 racewalklist(n->list, nil);
を呼び出すことで、ブロック内のすべてのステートメントを通常通り再帰的に走査し、計装を適用します。これにより、インライン化された関数などの内部も適切にレース検出の対象となります。
- 上記以外の
この変更により、Goコンパイラは、これまでスキップされていた特定のBLOCK
ノードに対してもレース検出の計装を適用できるようになり、レース検出器の網羅性と精度が向上します。
コアとなるコードの変更箇所
変更はsrc/cmd/gc/racewalk.c
ファイル内のracewalknode
関数に集中しています。
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -88,6 +88,7 @@ static void
racewalknode(Node **np, NodeList **init, int wr, int skip)\n {\n \tNode *n, *n1;\n+\tNodeList *fini;\n \n \tn = *np;\n \n@@ -116,8 +117,28 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)\n \t\tgoto ret;\n \n \tcase OBLOCK:\n-\t\t// leads to crashes.\n-\t\t//racewalklist(n->list, nil);\n+\t\tif(n->list == nil)\n+\t\t\tgoto ret;\n+\n+\t\tswitch(n->list->n->op) {\n+\t\tcase OCALLFUNC:\n+\t\tcase OCALLMETH:\n+\t\tcase OCALLINTER:\n+\t\t\t// Blocks are used for multiple return function calls.\n+\t\t\t// x, y := f() becomes BLOCK{CALL f, AS x [SP+0], AS y [SP+n]}\n+\t\t\t// We don\'t want to instrument between the statements because it will\n+\t\t\t// smash the results.\n+\t\t\tracewalknode(&n->list->n, &n->ninit, 0, 0);\n+\t\t\tfini = nil;\n+\t\t\tracewalklist(n->list->next, &fini);\n+\t\t\tn->list = concat(n->list, fini);\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\t// Ordinary block, for loop initialization or inlined bodies.\n+\t\t\tracewalklist(n->list, nil);\n+\t\t\tbreak;\n+\t\t}\n \t\tgoto ret;\n \n \tcase ODEFER:\
コアとなるコードの解説
変更のポイントは、case OBLOCK:
の部分です。
-
NodeList *fini;
の追加:racewalknode
関数のローカル変数としてfini
というNodeList
ポインタが追加されました。これは、関数呼び出しのブロックを処理する際に、後続のステートメントの計装結果を一時的に保持するために使用されます。
-
OBLOCK
処理の変更:-
変更前は、
OBLOCK
ケースは単にコメントアウトされたracewalklist(n->list, nil);
を含んでおり、実質的に何も処理していませんでした。 -
変更後は、まず
if(n->list == nil) goto ret;
で空のブロックをスキップします。 -
次に、
switch(n->list->n->op)
文が導入され、ブロック内の最初のノードの操作(op
)に基づいて処理を分岐します。 -
case OCALLFUNC:
case OCALLMETH:
case OCALLINTER:
:- これらのケースは、通常の関数呼び出し、メソッド呼び出し、インターフェースメソッド呼び出しを表します。
racewalknode(&n->list->n, &n->ninit, 0, 0);
- これは、ブロックの最初の要素である関数呼び出しノード自体を走査し、計装します。
n->ninit
は初期化リスト、0, 0
はそれぞれ書き込みフラグとスキップフラグです。
- これは、ブロックの最初の要素である関数呼び出しノード自体を走査し、計装します。
fini = nil; racewalklist(n->list->next, &fini);
- 関数呼び出しに続く、ブロック内の残りのステートメント(例: 複数の戻り値の変数への割り当て)を走査し、計装します。計装されたノードは
fini
リストに追加されます。
- 関数呼び出しに続く、ブロック内の残りのステートメント(例: 複数の戻り値の変数への割り当て)を走査し、計装します。計装されたノードは
n->list = concat(n->list, fini);
- 元のブロックリスト(関数呼び出しノードを含む)と、後続のステートメントの計装結果を含む
fini
リストを結合し、新しいブロックリストとしてn->list
に設定します。これにより、関数呼び出しと戻り値の割り当ての間に計装コードが挿入されることを防ぎつつ、両方が適切に処理されます。
- 元のブロックリスト(関数呼び出しノードを含む)と、後続のステートメントの計装結果を含む
-
default:
:- 上記以外の
OBLOCK
ノード(例: ループの初期化、インライン化された関数の本体など)は、通常のブロックとして扱われます。 racewalklist(n->list, nil);
- ブロック内のすべてのステートメントを再帰的に走査し、計装を適用します。これは、変更前のコメントアウトされていた行が、特定の条件で有効になった形です。
- 上記以外の
-
この変更により、Goのレース検出器は、これまで見逃されていた可能性のあるデータ競合を、より正確に検出できるようになります。
関連リンク
- Go Race Detector: https://go.dev/blog/race-detector
- ThreadSanitizer (TSan): https://github.com/google/sanitizers/wiki/ThreadSanitizerCppDynamicAnnotations
- Go言語のコンパイラに関するドキュメント(公式):https://go.dev/doc/compiler
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
src/cmd/gc/racewalk.c
) - ThreadSanitizerに関する一般的な情報
- データ競合と並行プログラミングに関する一般的な知識
- コミットメッセージと関連するGoのコードレビュー(CL: Change List): https://golang.org/cl/6821068 (これはコミットメッセージに記載されているリンクであり、詳細な議論が含まれている可能性があります。)
- Goコンパイラの内部構造に関するブログ記事や解説(一般的な知識として)