[インデックス 14345] ファイルの概要
コミット
commit f59a605645def8e5afd5052e0e47836921c59c05
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Nov 7 21:35:21 2012 +0100
runtime: use runtime·callers when racefuncenter's pc is on the heap.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6821069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f59a605645def8e5afd5052e0e47836921c59c05
元コミット内容
このコミットは、Goランタイムのデータ競合検出機能(race detector)において、runtime·racefuncenter
関数が呼び出し元のプログラムカウンタ(PC)を特定する際のロジックを改善するものです。具体的には、PCがスタック上にない場合(runtime·lessstack
の場合)に加えて、PCがヒープ上にある場合(クロージャのトランポリン関数など)にも、より低速だが正確な runtime·callers
関数を使用して実際の呼び出し元を特定するように変更しています。
変更の背景
Goのランタイムには、並行処理におけるデータ競合を検出するための「race detector」が組み込まれています。この検出器は、メモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告を発します。
runtime·racefuncenter
は、関数が実行される際に呼び出され、その関数の呼び出し元の情報を取得しようとします。通常、呼び出し元のプログラムカウンタ(PC)はスタック上に存在し、効率的に取得できます。しかし、特定の状況下ではPCがスタック上に直接存在しないことがあります。
元の実装では、PCが runtime·lessstack
という特殊な値である場合にのみ、より複雑なスタックウォークを行う runtime·callers
を使用していました。runtime·lessstack
は、スタックが分割された際に、呼び出し元が別のスタックセグメントにあることを示すマーカーとして機能します。
このコミットの背景には、クロージャ(closure)の実行に関する問題があったと考えられます。Goのクロージャは、その定義されたスコープ外で実行される際に、通常、ヒープ上に割り当てられた「トランポリン関数」と呼ばれる小さなコードスニペットを介して呼び出されます。このトランポリン関数は、実際のクロージャのコードへのジャンプを処理します。
もし runtime·racefuncenter
が、このようなヒープ上のトランポリン関数のPCを受け取った場合、従来のスタックベースのPC取得ロジックでは正しい呼び出し元を特定できませんでした。これは、PCがスタック上ではなくヒープメモリ領域に存在するためです。この不正確なPC情報が、race detectorの誤検出や検出漏れにつながる可能性がありました。
この変更は、このようなヒープ上のPC(特にクロージャのトランポリン)の場合にも runtime·callers
を使用することで、race detectorが常に正確な呼び出し元情報を取得できるようにし、検出の信頼性を向上させることを目的としています。
前提知識の解説
-
プログラムカウンタ (PC): CPUが次に実行する命令のアドレスを保持するレジスタです。関数呼び出しの際には、呼び出し元のPCがスタックに保存され、関数から戻る際にそのアドレスにジャンプして実行を再開します。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、ゴルーチン(goroutine)のスケジューリング、チャネル通信、スタック管理など、Go言語の並行処理やメモリ管理の根幹を担っています。C言語で書かれた部分とGo言語で書かれた部分があります。
-
Goのスタック管理とスタック分割 (Stack Management and Stack Splitting): Goのゴルーチンは、最初は小さなスタック(数KB)で開始されます。関数呼び出しが深くネストされ、スタックが不足しそうになると、ランタイムは自動的に新しい、より大きなスタックセグメントを割り当て、既存のスタックの内容をコピーして拡張します。これを「スタック分割」と呼びます。このプロセスにより、スタックオーバーフローを心配することなく、多数のゴルーチンを効率的に実行できます。
runtime·lessstack
は、このスタック分割の境界を示すために使用される内部的なマーカーです。 -
ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。スタックとは異なり、ヒープに割り当てられたメモリは、関数呼び出しの終了後も保持されます。Goでは、ガベージコレクタがヒープメモリの管理を行います。
-
クロージャ (Closure): 関数と、その関数が定義された環境(スコープ)にある非ローカル変数への参照を組み合わせたものです。Goでは、クロージャは関数リテラルとして定義され、そのリテラルが変数に代入されたり、引数として渡されたりすると、そのクロージャはヒープ上に割り当てられることがあります。
-
トランポリン関数 (Trampoline Function): 特定の状況下で、間接的な関数呼び出しを仲介するために生成される小さなコードスニペットです。クロージャがヒープ上に割り当てられ、そのクロージャが呼び出される際に、実際のクロージャのコードへジャンプするためにトランポリン関数が使用されることがあります。このトランポリン関数自体はヒープメモリ上に存在します。
-
runtime·callers
関数: Goランタイムの内部関数で、現在のゴルーチンのコールスタックをウォークし、指定された数の呼び出し元のプログラムカウンタ(PC)を取得します。これは比較的低レベルで、スタック分割を考慮して正確なPCを特定できますが、スタックをウォークするため、通常のPC取得よりもオーバーヘッドが大きいです。 -
runtime·mheap.arena_start
とruntime·mheap.arena_used
: これらはGoランタイムのメモリ管理構造体mheap
の一部です。mheap
はヒープメモリの管理を担当します。arena_start
: ヒープアリーナ(Goが動的にメモリを割り当てる大きな領域)の開始アドレスを示します。arena_used
: ヒープアリーナの現在使用されている領域の終了アドレス(または次の割り当てが開始されるアドレス)を示します。 これらの値を使って、特定のメモリアドレスがヒープ領域内にあるかどうかを判断できます。
技術的詳細
このコミットは、src/pkg/runtime/race.c
ファイル内の runtime·racefuncenter
関数を変更しています。この関数は、データ競合検出器が関数のエントリポイントで呼び出し元の情報を取得するために使用されます。
変更前は、runtime·racefuncenter
は以下の条件で runtime·callers
を呼び出していました。
if(pc == (uintptr)runtime·lessstack)
runtime·callers(2, &pc, 1);
これは、「もし呼び出し元のPCが runtime·lessstack
と等しい場合、つまりスタック分割の境界を越えて呼び出し元を探す必要がある場合、runtime·callers
を使って実際の呼び出し元PCを取得する」という意味です。runtime·callers(2, &pc, 1)
は、現在のPCから2フレーム遡って(runtime·racefuncenter
自体と、その呼び出し元をスキップして)、1つのPC(つまり、runtime·racefuncenter
を呼び出した関数の呼び出し元)を pc
変数に格納することを意味します。
このコミットでは、この条件に新しい句が追加されました。
if(pc == (uintptr)runtime·lessstack ||
(pc >= (uintptr)runtime·mheap.arena_start && pc < (uintptr)runtime·mheap.arena_used))
runtime·callers(2, &pc, 1);
追加された条件 (pc >= (uintptr)runtime·mheap.arena_start && pc < (uintptr)runtime·mheap.arena_used)
は、受け取った pc
がGoランタイムのヒープメモリ領域内にあるかどうかをチェックしています。
(uintptr)runtime·mheap.arena_start
: ヒープ領域の開始アドレスをuintptr
型にキャストしたもの。(uintptr)runtime·mheap.arena_used
: ヒープ領域の現在使用されている部分の終了アドレスをuintptr
型にキャストしたもの。
この新しい条件が真となるのは、pc
がヒープ領域の開始アドレス以上であり、かつ使用されている領域の終了アドレス未満である場合です。これは、pc
がヒープ上に割り当てられたコード(例えば、クロージャのトランポリン関数)を指している可能性が高いことを意味します。
なぜこの変更が必要か?
前述の通り、クロージャはヒープ上に割り当てられたトランポリン関数を介して呼び出されることがあります。このような場合、runtime·racefuncenter
に渡される pc
は、スタック上ではなくヒープ上のコードのアドレスになります。従来の pc == (uintptr)runtime·lessstack
という条件だけでは、このヒープ上のPCを適切に処理できず、runtime·callers
が呼び出されませんでした。その結果、race detectorは不正確な呼び出し元情報に基づいて動作し、データ競合の検出精度が低下する可能性がありました。
この変更により、PCがヒープ上にある場合でも runtime·callers
が強制的に呼び出されるようになり、スタックウォークによって正確な呼び出し元PCが特定されるようになります。これにより、race detectorはより信頼性の高い情報に基づいて動作し、データ競合の検出能力が向上します。
この修正は、Goのランタイムが、スタックとヒープの両方で発生しうる複雑な呼び出しパターン(特にクロージャのような動的なコード生成を伴うもの)を正確に処理できるようにするための重要なステップです。
コアとなるコードの変更箇所
変更は src/pkg/runtime/race.c
ファイルの以下の部分です。
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -89,7 +89,10 @@ runtime·racefuncenter(uintptr pc)
{\n \t// If the caller PC is lessstack, use slower runtime·callers\n \t// to walk across the stack split to find the real caller.\n-\tif(pc == (uintptr)runtime·lessstack)\n+\t// Same thing if the PC is on the heap, which should be a\n+\t// closure trampoline.\n+\tif(pc == (uintptr)runtime·lessstack ||\n+\t\t(pc >= (uintptr)runtime·mheap.arena_start && pc < (uintptr)runtime·mheap.arena_used))\n \t\truntime·callers(2, &pc, 1);\n \n \tm->racecall = true;\n```
## コアとなるコードの解説
変更された行は、`runtime·racefuncenter` 関数内の条件分岐です。
元のコード:
`if(pc == (uintptr)runtime·lessstack)`
これは、渡されたプログラムカウンタ `pc` が `runtime·lessstack` という特殊な値と一致するかどうかをチェックしていました。`runtime·lessstack` は、スタックが分割された際に、呼び出し元が現在のスタックセグメントとは異なる場所にあることを示すマーカーです。この条件が真の場合、ランタイムは `runtime·callers` を呼び出して、スタックを遡り、実際の呼び出し元PCを正確に特定していました。
変更後のコード:
`if(pc == (uintptr)runtime·lessstack || (pc >= (uintptr)runtime·mheap.arena_start && pc < (uintptr)runtime·mheap.arena_used))`
この変更では、既存の条件に `||` (論理OR) を使って新しい条件が追加されています。
新しい条件 `(pc >= (uintptr)runtime·mheap.arena_start && pc < (uintptr)runtime·mheap.arena_used)` は、`pc` がGoランタイムのヒープメモリ領域内にあるかどうかをチェックします。
- `runtime·mheap.arena_start`: ヒープ領域の開始アドレス。
- `runtime·mheap.arena_used`: ヒープ領域の現在使用されている部分の終了アドレス。
この条件が真となるのは、`pc` がヒープの有効なアドレス範囲内にある場合です。
この論理OR結合により、`runtime·racefuncenter` は以下のいずれかの条件が満たされた場合に `runtime·callers` を呼び出すようになります。
1. `pc` が `runtime·lessstack` である(スタック分割を越えて呼び出し元を探す必要がある)。
2. `pc` がヒープメモリ領域内にある(クロージャのトランポリン関数など、ヒープ上のコードを指している)。
これにより、race detectorが呼び出し元のPCを特定する際の堅牢性が向上し、スタック上だけでなくヒープ上のコードからの呼び出しも正確に追跡できるようになります。特に、クロージャのような動的に生成されヒープに配置される可能性のあるコードの正確な競合検出に寄与します。
## 関連リンク
* Go言語の公式ドキュメント: [https://go.dev/](https://go.dev/)
* GoのRace Detectorに関する公式ブログ記事: [https://go.dev/blog/race-detector](https://go.dev/blog/race-detector)
* Goのスタック管理に関する議論(Goの内部実装に詳しい方向け): [https://go.dev/src/runtime/stack.go](https://go.dev/src/runtime/stack.go) (Goソースコード)
## 参考にした情報源リンク
* Goのコミット履歴: [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
* GoのIssue Tracker: [https://go.dev/issue](https://go.dev/issue)
* Goのコードレビューシステム (Gerrit): [https://go-review.googlesource.com/](https://go-review.googlesource.com/)
* Goの内部実装に関するブログや記事 (例: "Go's Execution Tracer", "Go's Memory Allocator")
* Stack Overflowや技術フォーラムでのGoランタイム、クロージャ、race detectorに関する議論。
* Goのソースコード (`src/runtime/` ディレクトリ内のファイル、特に `race.c`, `stack.go`, `mheap.go` など)。
* Goのコンパイラとランタイムの設計に関する書籍や論文。
* `runtime·callers` の具体的な動作に関するGoのソースコードコメントや関連するコミット。
* `runtime·mheap` 構造体とメモリ管理に関するGoのソースコード。
* クロージャの内部実装とトランポリン関数に関するGoのコンパイラ設計資料。
* `runtime·lessstack` の役割に関するGoのスタック管理のドキュメント。