Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)に置き換えるというものです。これにより、メモリアクセスや関数エントリー/エグジットといった頻繁に発生する「ホットな」競合検出呼び出しのオーバーヘッドを削減し、パフォーマンスを向上させることが目的でした。

コミットメッセージには、regexpcompress/bzip2encoding/jsonといった標準ライブラリのベンチマーク結果が示されており、カスタムサック導入後に実行時間が大幅に短縮されていることが確認できます(それぞれ40%、50%、43%の改善)。これは、CGO呼び出しのオーバーヘッドがGoのRace Detectorの性能に大きく影響していたことを示唆しています。

また、この変更はcgocallおよびスケジューラから競合検出器関連の特殊なケースを削除し、将来的にはruntimecmd/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を日常的に使用する上での障壁となり、データ競合の早期発見を妨げる可能性がありました。

このコミットの背景には、以下の課題意識があったと考えられます。

  1. パフォーマンスの改善: CGO呼び出しのオーバーヘッドを削減し、Race Detectorの実行速度を向上させることで、開発者がより気軽にRace Detectorを利用できるようにする。
  2. ランタイムの簡素化: cgocallやスケジューラにおけるRace Detector関連の特殊な処理を排除し、Goランタイムのコードベースを簡素化する。
  3. 依存関係の解消: 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.csrc/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はより実用的なツールとなり、開発者はより頻繁にデータ競合のチェックを行うことができるようになりました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。

  1. src/pkg/runtime/race.c:

    • CGOを介したTSan関数呼び出しのラッパー関数(runtime∕race·Initializeなど)が削除され、代わりに__tsan_*というTSanライブラリの直接の関数名が宣言されています。
    • m->racecallフラグの使用が削除され、m->locksのインクリメント/デクリメントも不要になっています。
    • 新しいruntime·racecall関数が導入され、これがカスタムアセンブリサックを介してTSan関数を呼び出すための統一されたインターフェースとなります。
    • runtime·racesymbolizethunkというシンボライズコールバック関数が追加され、CからGoへのコールバックを処理します。
    • onstack関数の削除。
  2. 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へのコールバック時にスタックとゴルーチンコンテキストを適切に切り替える役割を担います。
  3. src/pkg/runtime/cgocall.c:

    • m->racecallフラグに関連する条件分岐や特殊な処理が削除されています。これにより、cgocallのコードが簡素化されています。
  4. src/pkg/runtime/proc.c:

    • runtime·sigprof関数内のm->racecallに関連する条件分岐が削除されています。
  5. src/pkg/runtime/race.h:

    • runtime·racefree関数の宣言が削除されています。
    • m->racecallフラグの定義が削除されています。
  6. src/pkg/runtime/race/race.go:

    • CGOを介してTSan関数を呼び出していたGoのラッパー関数がすべて削除されています。このファイルは、Race Detectorのビルド時にruntime/cgoをリンクさせるためのプレースホルダー的な役割のみを持つようになります。

コアとなるコードの解説

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<>ルーチンは、以下の処理を行います。

  1. 現在のゴルーチンコンテキスト(g->racectx)をRARG0レジスタに設定します。
  2. アクセスされたアドレス(RARG1)がヒープまたはグローバルデータセグメント内にあるかをチェックします。スタック上のアドレスであれば、競合検出は行わずにすぐにリターンします。
  3. racecall<>ルーチンにジャンプします。

racecall<>ルーチンは、以下の処理を行います。

  1. 現在のOSスレッド(M)とゴルーチン(G)の情報を取得します。
  2. 現在のスタックポインタ(SP)を保存し、g0スタックに切り替えます。
  3. AXレジスタに格納されているTSan関数のアドレス(例: __tsan_read)をCALL命令で呼び出します。
  4. TSan関数の実行後、元のスタックポインタに戻します。
  5. 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ランタイムの全体的なパフォーマンスと保守性が向上しました。

関連リンク

参考にした情報源リンク

  • コミットメッセージ: 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)