[インデックス 18783] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)の内部実装に関する重要な変更です。具体的には、データ競合検出器がGoランタイムの関数を呼び出す際に、従来のCGO(C Foreign Function Interface)を介した呼び出しから、カスタムアセンブリサック(thunks)を使用する方式へと移行しています。これにより、パフォーマンスが大幅に向上し、CGOとランタイム間の循環的な依存関係が解消され、コードベースの複雑性が軽減されています。
コミット
commit a1695d2ea321e9bed50d90732a8cef5e71cd7a89
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 6 23:48:30 2014 +0400
runtime: use custom thunks for race calls instead of cgo
Implement custom assembly thunks for hot race calls (memory accesses and function entry/exit).
The thunks extract caller pc, verify that the address is in heap or global and switch to g0 stack.
Before:
ok regexp 3.692s
ok compress/bzip2 9.461s
ok encoding/json 6.380s
After:
ok regexp 2.229s (-40%)
ok compress/bzip2 4.703s (-50%)
ok encoding/json 3.629s (-43%)
For comparison, normal non-race build:
ok regexp 0.348s
ok compress/bzip2 0.304s
ok encoding/json 0.661s
Race build:
ok regexp 2.229s (+540%)
ok compress/bzip2 4.703s (+1447%)
ok encoding/json 3.629s (+449%)
Also removes some race-related special cases from cgocall and scheduler.
In long-term it will allow to remove cyclic runtime/race dependency on cmd/cgo.
Fixes #4249.
Fixes #7460.
Update #6508
Update #6688
R=iant, rsc, bradfitz
CC=golang-codereviews
https://golang.org/cl/55100044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a1695d2ea321e9bed50d90732a8cef5e71cd7a89
元コミット内容
このコミットの元々の内容は、Goのデータ競合検出器がCGOを介してThreadSanitizer(TSan)ライブラリの関数を呼び出していた部分を、カスタムアセンブリサック(thunks)に置き換えるというものです。これにより、メモリアクセスや関数エントリー/エグジットといった頻繁に発生する「ホットな」競合検出呼び出しのオーバーヘッドを削減し、パフォーマンスを向上させることが目的でした。
コミットメッセージには、regexp
、compress/bzip2
、encoding/json
といった標準ライブラリのベンチマーク結果が示されており、カスタムサック導入後に実行時間が大幅に短縮されていることが確認できます(それぞれ40%、50%、43%の改善)。これは、CGO呼び出しのオーバーヘッドがGoのRace Detectorの性能に大きく影響していたことを示唆しています。
また、この変更はcgocall
およびスケジューラから競合検出器関連の特殊なケースを削除し、将来的にはruntime
とcmd/cgo
間の循環的な依存関係を解消する道を開くとも述べられています。
変更の背景
Goのデータ競合検出器は、プログラム実行中に発生するデータ競合(複数のゴルーチンが同時に共有メモリにアクセスし、少なくとも一方が書き込み操作を行う場合に発生する競合状態)を検出するためのツールです。この検出器は、LLVMのThreadSanitizer(TSan)ライブラリをベースにしています。
初期のGo Race Detectorの実装では、GoランタイムからTSanライブラリのC関数を呼び出す際に、CGO(C Foreign Function Interface)を使用していました。CGOはGoとC/C++コードを連携させるための強力なメカニズムですが、その呼び出しには一定のオーバーヘッドが伴います。特に、メモリアクセスや関数呼び出し/終了といった頻繁に発生するイベントに対して競合検出を行う場合、このCGOのオーバーヘッドが累積され、プログラムの実行速度を著しく低下させる原因となっていました。
コミットメッセージに示されているように、Race Detectorを有効にしたビルドでは、通常のビルドと比較して実行時間が数百パーセントも増加していました。この大きなオーバーヘッドは、開発者がRace Detectorを日常的に使用する上での障壁となり、データ競合の早期発見を妨げる可能性がありました。
このコミットの背景には、以下の課題意識があったと考えられます。
- パフォーマンスの改善: CGO呼び出しのオーバーヘッドを削減し、Race Detectorの実行速度を向上させることで、開発者がより気軽にRace Detectorを利用できるようにする。
- ランタイムの簡素化:
cgocall
やスケジューラにおけるRace Detector関連の特殊な処理を排除し、Goランタイムのコードベースを簡素化する。 - 依存関係の解消:
runtime
パッケージとcmd/cgo
パッケージ間の循環的な依存関係を解消し、Goのビルドシステムとモジュール構造をよりクリーンにする。
これらの課題を解決するために、CGOを介さずに直接TSanライブラリの関数を呼び出すためのカスタムアセンブリサックが導入されることになりました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
1. Go Race Detector (ThreadSanitizer)
Go Race Detectorは、Goプログラムにおけるデータ競合を検出するためのツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生します。データ競合は、プログラムの予測不能な動作やバグの原因となるため、Go Race Detectorは並行処理のデバッグにおいて非常に重要な役割を果たします。
Go Race Detectorは、コンパイル時にコードに計測(instrumentation)を挿入することで機能します。この計測されたコードは、メモリアクセスや同期操作が発生するたびに、内部のThreadSanitizer(TSan)ライブラリにイベントを報告します。TSanライブラリはこれらのイベントを追跡し、データ競合のパターンを検出すると警告を発します。
2. CGO (C Foreign Function Interface)
CGOは、GoプログラムからC言語の関数を呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムです。Goの標準ライブラリの一部(例えば、os/user
パッケージや一部のネットワーク関連パッケージ)でもCGOが内部的に使用されています。
CGOを使用するには、Goコード内にimport "C"
という特殊なインポート文を記述し、C言語の関数宣言や構造体定義をコメント形式で記述します。Goのビルドツールは、これらのC言語のコードをコンパイルし、Goコードとリンクします。
CGO呼び出しには、GoとCのスタック切り替え、レジスタの保存/復元、メモリモデルの調整など、いくつかのオーバーヘッドが伴います。そのため、非常に頻繁に呼び出されるホットパスでのCGOの使用は、パフォーマンスに大きな影響を与える可能性があります。
3. アセンブリサック (Assembly Thunks)
サック(Thunk)とは、別の関数を呼び出すための中間的なコード片のことです。このコミットでは「カスタムアセンブリサック」が導入されています。これは、CGOのオーバーヘッドを回避するために、Goランタイムが直接TSanライブラリのC関数を呼び出すための、手書きのアセンブリコードで書かれた小さな関数を指します。
アセンブリサックは、Goの呼び出し規約とCの呼び出し規約の間で、引数の渡し方やレジスタの使用方法を調整する役割を担います。これにより、CGOの複雑なスタック切り替えやスケジューラとの連携を介さずに、より直接的かつ高速にC関数を呼び出すことが可能になります。
4. g0
スタック
Goランタイムには、通常のゴルーチンが使用するスタック(g.stack
)とは別に、特別な目的で使用されるg0
スタックが存在します。g0
スタックは、スケジューラ、ガベージコレクタ、シグナルハンドラなど、Goランタイムの低レベルな処理を実行するために使用されます。
通常のゴルーチンは、システムコールやCGO呼び出しを行う際に、g0
スタックに切り替わることがあります。これは、GoのスケジューラがCGO呼び出し中にゴルーチンをプリエンプト(中断)できないため、Cコードの実行中にGoのスケジューラがブロックされないようにするためです。
このコミットでは、カスタムアセンブリサックがg0
スタックに切り替えてTSan関数を呼び出すことで、Goのスケジューラとの連携をより効率的に行い、m->racecall
のような特殊なフラグを不要にしています。
5. m->racecall
フラグ
このコミット以前のGoランタイムでは、M
構造体(OSスレッドを表すランタイム内部の構造体)にracecall
というブール型のフラグが存在しました。このフラグは、現在のOSスレッドがRace DetectorのCGO呼び出しを実行中であることを示すために使用されていました。
このフラグは、cgocall
やスケジューラがRace Detectorの特殊な動作を考慮するために使用されていましたが、カスタムアセンブリサックの導入により、このような特殊な処理が不要になったため、このフラグは削除されました。
技術的詳細
このコミットの技術的な核心は、GoランタイムがThreadSanitizer(TSan)ライブラリのC関数を呼び出す際のメカニズムを、CGOからカスタムアセンブリサックに切り替えた点にあります。
1. CGO呼び出しの課題
従来のCGO呼び出しでは、GoのコードからC関数を呼び出す際に、以下のような処理が内部的に行われていました。
- スタック切り替え: Goのスタック(Goルーチンのスタック)からCのスタック(OSスレッドのスタック)への切り替え。
- レジスタの保存/復元: Goの呼び出し規約とCの呼び出し規約の違いを吸収するためのレジスタの保存と復元。
- スケジューラとの連携: CGO呼び出し中はGoのスケジューラがゴルーチンをプリエンプトできないため、ランタイムはCGO呼び出し中であることを認識し、スケジューリングの調整を行う必要がありました。これには、
m->racecall
のようなフラグが使用されていました。 - オーバーヘッド: これらの処理は、特に頻繁に発生するメモリアクセスや関数エントリー/エグジットのイベントごとに実行されるため、累積的なオーバーヘッドが大きくなっていました。
2. カスタムアセンブリサックの導入
このコミットでは、src/pkg/runtime/race_amd64.s
に手書きのアセンブリコードで書かれたカスタムサックが導入されました。これらのサックは、TSanライブラリの主要な関数(__tsan_read
, __tsan_write
, __tsan_func_enter
, __tsan_func_exit
など)を直接呼び出すためのものです。
カスタムサックの主な機能は以下の通りです。
- 呼び出し元PCの抽出: メモリアクセスや関数呼び出しのイベントが発生した際、サックは呼び出し元のプログラムカウンタ(PC)を正確に抽出します。これは、競合検出においてどのコードがアクセスを行ったかを特定するために重要です。
- アドレスの検証: アクセスされたアドレスがヒープまたはグローバル変数領域内にあることを検証します。スタック上の変数へのアクセスは通常、データ競合の対象外となるため、不要な検出を避けるための最適化です。
g0
スタックへの切り替え: TSan関数を呼び出す前に、現在のゴルーチンのスタックからg0
スタックに切り替えます。これにより、TSan関数が実行されている間もGoのスケジューラがブロックされることなく、他のゴルーチンのスケジューリングを継続できます。また、m->racecall
のような特殊なフラグが不要になります。- 直接呼び出し: CGOの複雑なパスを介さずに、TSanライブラリのC関数を直接呼び出します。これにより、CGOのオーバーヘッドが大幅に削減されます。
3. m->racecall
フラグの削除とランタイムの簡素化
カスタムアセンブリサックがg0
スタックへの切り替えを直接処理するようになったため、GoランタイムはRace Detectorの呼び出しがCGOを介しているかどうかを特別に意識する必要がなくなりました。これにより、M
構造体からracecall
フラグが削除され、src/pkg/runtime/cgocall.c
やsrc/pkg/runtime/proc.c
といったファイルから、racecall
フラグに関連する特殊な処理が削除されました。
この変更は、Goランタイムのコードベースを簡素化し、保守性を向上させる効果があります。
4. 循環依存の解消
コミットメッセージには「In long-term it will allow to remove cyclic runtime/race dependency on cmd/cgo.」とあります。これは、Goのビルドシステムにおける依存関係の問題を指しています。
runtime
パッケージは、CGOを介してTSanライブラリ(race
パッケージの一部)を呼び出していました。cmd/cgo
は、CGOのコードを処理するためのツールであり、Goのビルドプロセスの一部です。race
パッケージは、runtime
パッケージに依存していました。
このように、runtime
-> race
-> cmd/cgo
-> runtime
のような循環的な依存関係が存在していた可能性があります。カスタムアセンブリサックの導入により、runtime
が直接cmd/cgo
に依存することなくTSanライブラリを呼び出せるようになったため、この循環依存を解消する道が開かれました。
5. パフォーマンスの向上
カスタムアセンブリサックによる直接呼び出しは、CGO呼び出しのオーバーヘッドを回避するため、Race Detectorの実行速度を大幅に向上させました。コミットメッセージに示されているベンチマーク結果は、このパフォーマンス改善の具体的な証拠です。これにより、Race Detectorはより実用的なツールとなり、開発者はより頻繁にデータ競合のチェックを行うことができるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/pkg/runtime/race.c
:- CGOを介したTSan関数呼び出しのラッパー関数(
runtime∕race·Initialize
など)が削除され、代わりに__tsan_*
というTSanライブラリの直接の関数名が宣言されています。 m->racecall
フラグの使用が削除され、m->locks
のインクリメント/デクリメントも不要になっています。- 新しい
runtime·racecall
関数が導入され、これがカスタムアセンブリサックを介してTSan関数を呼び出すための統一されたインターフェースとなります。 runtime·racesymbolizethunk
というシンボライズコールバック関数が追加され、CからGoへのコールバックを処理します。onstack
関数の削除。
- CGOを介したTSan関数呼び出しのラッパー関数(
-
src/pkg/runtime/race_amd64.s
:- このファイルに、TSanライブラリの関数を直接呼び出すためのカスタムアセンブリサックが大量に追加されています。
runtime·raceread
,runtime·racewrite
,runtime·racefuncenter
,runtime·racefuncexit
などのGoランタイム関数が、これらのアセンブリサックを介してTSan関数(__tsan_read
,__tsan_write
など)を呼び出すように変更されています。racecalladdr<>
およびracecall<>
という共通のアセンブリルーチンが導入され、g0
スタックへの切り替えや引数の設定を処理します。runtime·racesymbolizethunk
の実装が追加され、CからGoへのコールバック時にスタックとゴルーチンコンテキストを適切に切り替える役割を担います。
-
src/pkg/runtime/cgocall.c
:m->racecall
フラグに関連する条件分岐や特殊な処理が削除されています。これにより、cgocall
のコードが簡素化されています。
-
src/pkg/runtime/proc.c
:runtime·sigprof
関数内のm->racecall
に関連する条件分岐が削除されています。
-
src/pkg/runtime/race.h
:runtime·racefree
関数の宣言が削除されています。m->racecall
フラグの定義が削除されています。
-
src/pkg/runtime/race/race.go
:- CGOを介してTSan関数を呼び出していたGoのラッパー関数がすべて削除されています。このファイルは、Race Detectorのビルド時に
runtime/cgo
をリンクさせるためのプレースホルダー的な役割のみを持つようになります。
- CGOを介してTSan関数を呼び出していたGoのラッパー関数がすべて削除されています。このファイルは、Race Detectorのビルド時に
コアとなるコードの解説
src/pkg/runtime/race.c
の変更点
以前は、runtime/race.c
にはruntime∕race·Initialize
のような関数があり、これらがCGOを介して__tsan_init
などのTSanライブラリ関数を呼び出していました。このコミットでは、これらのラッパー関数が削除され、代わりにTSanライブラリの関数が直接__tsan_init
などの名前で宣言されています。
// 変更前:
// void runtime∕race·Initialize(uintptr *racectx);
// void runtime∕race·MapShadow(void *addr, uintptr size);
// ...
// 変更後:
void __tsan_init(void);
void __tsan_fini(void);
void __tsan_map_shadow(void);
// ...
そして、これらのTSan関数を呼び出すための新しい統一されたインターフェースとしてruntime·racecall
が導入されました。
// racecall allows calling an arbitrary function f from C race runtime
// with up to 4 uintptr arguments.
void runtime·racecall(void(*f)(void), ...);
このruntime·racecall
は、可変引数を取り、内部でアセンブリサック(racecall<>
)を呼び出して実際のTSan関数を実行します。これにより、race.c
内の各Race Detector関連の関数は、CGOの複雑な処理を意識することなく、runtime·racecall
を介してTSan関数を呼び出すことができるようになりました。
例えば、runtime·raceinit
関数は以下のように変更されました。
// 変更前:
// m->racecall = true;
// m->locks++;
// runtime∕race·Initialize(&racectx);
// ...
// m->locks--;
// m->racecall = false;
// 変更後:
runtime·racecall(__tsan_init, &racectx, runtime·racesymbolizethunk);
m->racecall
フラグの設定とm->locks
の操作が削除され、代わりにruntime·racecall
が直接呼び出されています。これは、runtime·racecall
の内部でアセンブリサックがg0
スタックへの切り替えやレジスタの保存/復元を処理するため、Goランタイム側でこれらの特殊な処理を行う必要がなくなったことを意味します。
また、CからGoへのコールバック(例えば、TSanがシンボライズ情報が必要な場合)を処理するために、runtime·racesymbolizethunk
という関数が追加されました。この関数は、Cコードから呼び出され、Goのruntime·racesymbolize
関数を呼び出すための中間層として機能します。
src/pkg/runtime/race_amd64.s
の変更点
このファイルは、カスタムアセンブリサックの具体的な実装を含んでいます。
例えば、メモリ読み込みを検出するruntime·raceread
関数は以下のように変更されました。
// func runtime·raceread(addr uintptr)
// Called from instrumented code.
TEXT runtime·raceread(SB), NOSPLIT, $0-8
MOVQ addr+0(FP), RARG1
MOVQ (SP), RARG2
// void __tsan_read(ThreadState *thr, void *addr, void *pc);
MOVQ $__tsan_read(SB), AX
JMP racecalladdr<>(SB)
ここで注目すべきは、JMP racecalladdr<>(SB)
という命令です。これは、racecalladdr<>
という共通のアセンブリルーチンにジャンプすることを意味します。
racecalladdr<>
ルーチンは、以下の処理を行います。
- 現在のゴルーチンコンテキスト(
g->racectx
)をRARG0
レジスタに設定します。 - アクセスされたアドレス(
RARG1
)がヒープまたはグローバルデータセグメント内にあるかをチェックします。スタック上のアドレスであれば、競合検出は行わずにすぐにリターンします。 racecall<>
ルーチンにジャンプします。
racecall<>
ルーチンは、以下の処理を行います。
- 現在のOSスレッド(
M
)とゴルーチン(G
)の情報を取得します。 - 現在のスタックポインタ(
SP
)を保存し、g0
スタックに切り替えます。 AX
レジスタに格納されているTSan関数のアドレス(例:__tsan_read
)をCALL
命令で呼び出します。- TSan関数の実行後、元のスタックポインタに戻します。
RET
命令で呼び出し元に戻ります。
このように、アセンブリサックは、Goの呼び出し規約からCの呼び出し規約への変換、g0
スタックへの切り替え、レジスタの保存/復元といった低レベルな処理を直接行い、CGOのオーバーヘッドを回避しています。
また、runtime·racesymbolizethunk
のアセンブリ実装も重要です。これは、TSanライブラリがシンボライズ情報(関数名、ファイル名、行番号など)を必要とする際に、CコードからGoのruntime·racesymbolize
関数を呼び出すためのものです。このサックは、Cの呼び出し規約で渡された引数を受け取り、Goの呼び出し規約に合わせてレジスタを調整し、g0
スタックに切り替えてからGo関数を呼び出します。Go関数の実行後、元のコンテキストに戻るためのレジスタ復元も行われます。
その他のファイルの変更点
src/pkg/runtime/cgocall.c
:m->racecall
フラグが削除されたため、このフラグに基づく条件分岐(例えば、if(m->racecall)
)がすべて削除されました。これにより、CGO呼び出しのパスが簡素化され、Race Detectorの有無に関わらず一貫した動作をするようになりました。src/pkg/runtime/proc.c
: 同様に、runtime·sigprof
関数内のm->racecall
に関連する特殊な処理が削除されました。src/pkg/runtime/race/race.go
: このファイルは、以前はCGOのimport "C"
を使用してTSan関数をGoのラッパーとして公開していましたが、このコミットでその内容がすべて削除されました。これにより、runtime/race
パッケージは、Race Detectorのビルド時にruntime/cgo
をリンクさせるためのマーカーとしての役割のみを持つようになります。
これらの変更により、Go Race Detectorの内部実装はより効率的かつクリーンになり、Goランタイムの全体的なパフォーマンスと保守性が向上しました。
関連リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
- ThreadSanitizer (TSan): https://clang.llvm.org/docs/ThreadSanitizer.html
- Go CGO: https://go.dev/blog/cgo
- Go Runtime Internals (g0 stack, M, G): Goのランタイム内部に関する公式ドキュメントやブログ記事は多数存在しますが、特定のURLを挙げるのは難しいです。Goのソースコード(特に
src/runtime
ディレクトリ)を読むことが最も詳細な情報源となります。
参考にした情報源リンク
- コミットメッセージ:
commit_data/18783.txt
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- Go CGO Documentation: https://go.dev/blog/cgo
- Go Source Code (src/runtime): https://github.com/golang/go/tree/master/src/runtime
- ThreadSanitizer Documentation: https://clang.llvm.org/docs/ThreadSanitizer.html
- Go Issue #4249: runtime: race detector slow (https://github.com/golang/go/issues/4249)
- Go Issue #7460: runtime: race detector: remove cyclic dependency on cmd/cgo (https://github.com/golang/go/issues/7460)
- Go Issue #6508: runtime: race detector: improve performance (https://github.com/golang/go/issues/6508)
- Go Issue #6688: runtime: race detector: improve performance (https://github.com/golang/go/issues/6688)
- Go Code Review 55100044: runtime: use custom thunks for race calls instead of cgo (https://golang.org/cl/55100044)