[インデックス 14002] ファイルの概要
このコミットは、Go言語のコンパイラ gc
にデータ競合検出機能の初期部分を導入するものです。具体的には、コンパイラに -b
フラグが与えられた際に、生成されるバイナリコードにメモリアクセス(読み書き)および関数エントリー/エグジットの計測コードを挿入する機能を追加します。これは、Goのデータ競合検出ツール(Race Detector)の基盤となる重要な変更であり、racewalk.c
という新しいファイルが追加され、既存のコンパイラ関連ファイルが修正されています。
コミット
race: gc changes
This is the first part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
This change makes gc compiler instrument memory accesses when supplied with -b flag.
R=rsc, nigeltao, lvd
CC=golang-dev
https://golang.org/cl/6497074
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/041fc8bf96993d7405d938c7f4ad0b6ec474a91a
元コミット内容
commit 041fc8bf96993d7405d938c7f4ad0b6ec474a91a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Oct 2 10:05:46 2012 +0400
race: gc changes
This is the first part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
This change makes gc compiler instrument memory accesses when supplied with -b flag.
R=rsc, nigeltao, lvd
CC=golang-dev
https://golang.org/cl/6497074
変更の背景
並行プログラミングにおいて、複数のゴルーチンが共有メモリに同時にアクセスし、少なくとも一方が書き込み操作である場合に発生する「データ競合(Data Race)」は、プログラムの予測不能な動作やバグの主要な原因となります。Go言語は並行処理を強力にサポートしていますが、データ競合は依然として開発者が直面する課題でした。
このコミットは、Go言語に組み込みのデータ競合検出機能(Go Race Detector)を導入するための第一歩です。開発者がコンパイル時に特定のフラグ(-b
)を指定することで、実行時にデータ競合を検出できるようになることを目指しています。これにより、並行プログラムのデバッグと信頼性向上が大いに促進されます。コミットメッセージに記載されている通り、これはより大きな変更(https://golang.org/cl/6456044
)の一部であり、コンパイラレベルでの計測(instrumentation)がその基盤となります。
前提知識の解説
データ競合 (Data Race)
データ競合とは、以下の3つの条件がすべて満たされたときに発生する並行処理のバグです。
- 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込みである。
- これらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合が発生すると、プログラムの動作は未定義となり、結果が予測不能になったり、クラッシュしたりする可能性があります。Go言語では、sync
パッケージの Mutex
や RWMutex
、あるいはチャネルを用いた通信によって共有メモリへのアクセスを同期させることが推奨されています。
コンパイラによる計測 (Instrumentation)
計測とは、プログラムの実行時の振る舞いを監視するために、コンパイラやリンカが自動的に追加のコードを挿入する技術です。データ競合検出においては、メモリの読み書きや関数の呼び出し/終了といったイベントが発生するたびに、特別なランタイム関数を呼び出すコードを挿入します。これらのランタイム関数は、メモリアクセスの履歴を追跡し、競合パターンを検出します。
Goコンパイラ (gc)
gc
はGo言語の公式コンパイラであり、src/cmd/gc
ディレクトリにそのソースコードがあります。Goのソースコードを中間表現に変換し、最終的に機械語コードを生成する役割を担っています。このコミットでは、gc
がAST(抽象構文木)を走査し、計測コードを挿入する新しいパス(racewalk
)を追加しています。
uintptr
型
uintptr
はGo言語の組み込み型で、ポインタを整数として表現できる型です。ポインタ演算を行う際に使用されることがありますが、ガベージコレクタの対象外であるため、注意して使用する必要があります。データ競合検出においては、メモリアドレスをランタイム関数に渡す際に uintptr
に変換して使用されます。
技術的詳細
このコミットの主要な技術的変更点は、Goコンパイラ gc
にデータ競合検出のためのコード計測機能を追加したことです。
-
-b
フラグの導入:src/cmd/gc/doc.go
に、gc
コンパイラに-b
フラグを追加したことが明記されました。このフラグを付けてコンパイルすると、データ競合検出が有効になります。src/cmd/gc/lex.c
では、-b
フラグが指定された場合にruntime/race
パッケージをインポートし、パッケージパスに_race
サフィックスを追加するロジックが追加されました。これは、競合検出用に特別にコンパイルされたパッケージ(例えば、sync_race
など)をロードできるようにするためと考えられます。
-
racewalk
パスの追加:src/cmd/gc/pgen.c
のcompile
関数内で、-b
フラグが有効な場合にracewalk(curfn)
が呼び出されるようになりました。これは、racewalk
がコンパイルプロセスの後半で、各関数のASTに対して計測処理を行うことを意味します。src/cmd/gc/racewalk.c
の新規追加: このファイルがコミットの核心です。racewalk
関数は、コンパイル対象の関数(fn
)のASTを走査し、計測コードを挿入します。- 関数のエントリー時に
racefuncenter
を、エグジット時にracefuncexit
を呼び出すコードを挿入します。 racewalknode
およびcallinstr
関数は、ASTノードを再帰的に走査し、メモリの読み書き操作(OAS
,ODOT
,OIND
,OINDEX
,ONAME
など)を検出します。- 検出されたメモリ操作の前に、
raceread
またはracewrite
を呼び出すコードを挿入します。これらの関数には、アクセスされるメモリのアドレスがuintptr
型で渡されます。 uintptraddr
ヘルパー関数は、与えられたノードのアドレスを取得し、uintptr
型に変換します。basenod
ヘルパー関数は、ODOT
やOINDEX
のような複合的なアクセスの場合に、基底となるノード(例えば、構造体のフィールドアクセスにおける構造体変数自体)を特定します。- 計測の除外:
omitPkgs
配列("runtime"
,"runtime/race"
,"sync"
,"sync/atomic"
)にリストされているパッケージは、計測の対象外とされます。これは、これらのパッケージ自体が競合検出のランタイムを構成するため、あるいは低レベルの同期プリミティブを提供するため、計測すると無限ループや不正確な結果、過剰なオーバーヘッドを引き起こす可能性があるためです。
-
ランタイム関数の宣言:
src/cmd/gc/builtin.c
とsrc/cmd/gc/runtime.go
に、racefuncenter
,racefuncexit
,raceread
,racewrite
の関数シグネチャが追加されました。これらはGoのランタイム(おそらくCやアセンブリで実装される)によって提供される関数であり、コンパイラが生成するコードから呼び出されます。src/cmd/gc/go.h
には、runtime/race
パッケージを表すracepkg
の宣言が追加されました。src/cmd/gc/reflect.c
では、-b
フラグが有効な場合にracepkg
のインポートパスが追加されるようになりました。
これらの変更により、Goコンパイラは、データ競合検出に必要なフックをプログラムの実行パスに挿入できるようになり、Go Race Detectorの実現に向けた重要な一歩となりました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。
-
src/cmd/gc/racewalk.c
(新規ファイル):racewalk(Node *fn)
: 各関数のASTを走査し、計測コードを挿入するメイン関数。racewalklist(NodeList *l, NodeList **init)
: ノードリストを走査するヘルパー関数。racewalknode(Node **np, NodeList **init, int wr, int skip)
: 個々のASTノードを走査し、メモリアクセスを特定する再帰関数。callinstr(Node *n, NodeList **init, int wr, int skip)
: 実際に計測コード(raceread
またはracewrite
の呼び出し)を挿入する関数。uintptraddr(Node *n)
: ノードのアドレスをuintptr
に変換する関数。basenod(Node *n)
: 複合的なアクセス(例:a.b.c
やarr[i]
)の基底ノードを特定する関数。
-
src/cmd/gc/pgen.c
:compile(Node *fn)
: この関数内で、-b
フラグが有効な場合にracewalk(curfn)
が呼び出されます。
-
src/cmd/gc/builtin.c
およびsrc/cmd/gc/runtime.go
:racefuncenter()
,racefuncexit()
,raceread(uintptr)
,racewrite(uintptr)
の関数シグネチャが追加されました。これらはコンパイラが生成する計測コードから呼び出されるランタイム関数です。
コアとなるコードの解説
src/cmd/gc/racewalk.c
このファイルは、データ競合検出のためのコード計測(instrumentation)ロジックを実装しています。
-
racewalk(Node *fn)
:- この関数は、コンパイル中の関数
fn
のASTを受け取ります。 - まず、
omitPkgs
リストに含まれるパッケージ(runtime
,runtime/race
,sync
,sync/atomic
)の場合、計測をスキップします。これは、これらのパッケージが競合検出のランタイム自体を構成したり、低レベルの同期プリミティブを提供したりするため、計測すると不正確な結果や無限ループを引き起こす可能性があるためです。 - 関数の開始時に
racefuncenter()
を、終了時にracefuncexit()
を呼び出すコードをASTに挿入します。これにより、関数の実行範囲を追跡できます。 racewalklist(curfn->nbody, nil)
を呼び出し、関数の本体(nbody
)のASTを走査して、個々のステートメントや式に計測コードを挿入します。
- この関数は、コンパイル中の関数
-
racewalknode(Node **np, NodeList **init, int wr, int skip)
:- この関数は、ASTノード
n
を再帰的に走査します。 wr
パラメータは、現在の操作が書き込み(1)か読み込み(0)かを示します。skip
パラメータは、計測をスキップすべきかどうかを示します。switch(n->op)
文で様々なASTノードのタイプを処理します。OASOP
,OAS
,OAS2
などの代入操作では、左辺(書き込み)と右辺(読み込み)に対して再帰的にracewalknode
を呼び出します。ODOT
,ODOTPTR
,OIND
,OINDEX
,ONAME
などのメモリアクセスを表すノードに対しては、callinstr
を呼び出して計測コードを挿入します。OFOR
,OIF
,OSWITCH
などの制御フローノードでは、条件式やボディ、初期化リストなどを再帰的に走査します。OCALLFUNC
,OCALLINTER
,OCALLMETH
などの関数呼び出しでは、引数リストなどを走査します。OLEN
,OCAP
など、一部の組み込み関数は、その引数に対して計測を行います。OADDR
(アドレス取得) の場合は、その対象自体へのアクセスではないため、skip
フラグを立ててracewalknode
を再帰呼び出しします。OINDEXMAP
,OPRINT
など、一部の操作は計測をスキップします。OCMPSTR
,OAPPEND
など、まだ実装されていない(unimplemented
)操作もスキップされます。
- この関数は、ASTノード
-
callinstr(Node *n, NodeList **init, int wr, int skip)
:- この関数は、実際に
raceread
またはracewrite
の呼び出しを挿入するかどうかを決定します。 skip
が真の場合や、ノードの型が不明な場合、あるいは理想型(TIDEAL
)の場合は計測をスキップします。ONAME
ノードの場合、アンダースコアで始まる変数名(_
やautotmp_
,statictmp_
)は計測をスキップします。これらはコンパイラが生成する一時変数や未使用変数であり、通常は競合の対象とならないためです。- 構造体(
TSTRUCT
)の場合、そのフィールドを再帰的に走査し、各フィールドへのアクセスを計測します。 basenod(n)
を呼び出して、アクセス対象の基底ノードを取得します。- 基底ノードのクラス(
class
)がPHEAP
(ヒープ上の変数)、PPARAMREF
(参照渡しされたパラメータ)、PEXTERN
(外部変数)、または配列(TARRAY
)、ポインタ経由のアクセス(ODOTPTR
,OIND
,OXDOT
)である場合にのみ、計測コードを挿入します。これは、スタック上のローカル変数へのアクセスは通常、単一ゴルーチン内でのみ行われるため、競合の対象とならないという前提に基づいています。 mkcall
を使用して、wr
の値に応じてracewrite
またはraceread
の呼び出しノードを作成します。uintptraddr(n)
を呼び出して、アクセスされるメモリのアドレスをuintptr
型で取得し、ランタイム関数に引数として渡します。- 生成された呼び出しノードは、現在のノードの初期化リスト(
init
)に追加されます。
- この関数は、実際に
src/cmd/gc/pgen.c
compile
関数は、GoのソースコードからASTを生成し、様々な最適化や変換パスを適用するコンパイラの主要な部分です。- このコミットでは、
racewalk(curfn)
の呼び出しが追加され、-b
フラグが有効な場合にデータ競合検出の計測パスが実行されるようになりました。これにより、コンパイルされたプログラムに競合検出のためのフックが組み込まれます。
src/cmd/gc/builtin.c
および src/cmd/gc/runtime.go
これらのファイルは、コンパイラが生成するコードから呼び出されるランタイム関数の宣言を提供します。
builtin.c
は、Goの組み込み関数やランタイム関数をコンパイラが認識するためのC言語の定義を含んでいます。ここにracefuncenter
,racefuncexit
,raceread
,racewrite
の宣言が追加されたことで、コンパイラはこれらの関数がGoのランタイムに存在することを認識し、適切に呼び出しコードを生成できるようになります。runtime.go
は、Goのランタイムパッケージの一部であり、Go言語で書かれたランタイム関数の宣言を含んでいます。ここにこれらの関数が宣言されたことで、Goのコードからこれらの関数を呼び出すことが可能になります(ただし、実際のGo Race Detectorのランタイム実装は、より低レベルのCやアセンブリで書かれることが多いです)。
これらの変更は、Goコンパイラがデータ競合検出機能をサポートするための基盤を構築し、Goプログラムの並行処理のデバッグと信頼性向上に大きく貢献します。
関連リンク
- 最初の変更セット: https://golang.org/cl/6456044
- このコミットの変更セット: https://golang.org/cl/6497074
参考にした情報源リンク
- Go Race Detector (公式ドキュメント): https://go.dev/doc/articles/race_detector
- Go言語の並行処理: https://go.dev/tour/concurrency/1
- Goコンパイラの内部構造 (一般的な情報源): https://go.dev/src/cmd/compile/README (Goのバージョンによって内容は異なりますが、一般的な構造理解に役立ちます)
- Dmitriy Vyukovのブログ (Go Race Detectorに関する初期の議論): https://www.google.com/search?q=Dmitriy+Vyukov+Go+Race+Detector (直接的なリンクではありませんが、関連情報を見つけるのに役立ちます)