[インデックス 14319] ファイルの概要
このコミットは、Goランタイムのレース検出器(Race Detector)に関連するコールバック関数にnosplitstack
属性を付与することで、パフォーマンスを向上させ、getcallerpc()
が不正なスタックポインタを返す問題を回避することを目的としています。具体的には、runtime·racewrite
、runtime·raceread
、runtime·racefuncenter
、runtime·racefuncexit
といったレース検出器の計測コールバック関数がスタック分割を行わないように変更されています。
コミット
- コミットハッシュ:
0ead18c59e357d79f10e3132d4b1b2fede577cbb
- Author: Dmitriy Vyukov dvyukov@google.com
- Date: Tue Nov 6 20:54:22 2012 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0ead18c59e357d79f10e3132d4b1b2fede577cbb
元コミット内容
runtime: mark race instrumentation callbacks as nosplitstack
It speedups the race detector somewhat, but also prevents
getcallerpc() from obtaining lessstack().
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/6812091
変更の背景
この変更には主に二つの背景があります。
-
レース検出器のパフォーマンス向上: Goのレース検出器は、並行処理におけるデータ競合(data race)を検出するための強力なツールです。しかし、その性質上、実行時に追加のオーバーヘッドを発生させます。このコミットでは、レース検出器の計測コールバック関数がスタック分割(stack splitting)を行わないようにすることで、そのオーバーヘッドを削減し、検出器の速度を向上させることを目指しています。スタック分割は、Goルーチンのスタックが動的に拡張されるメカニズムであり、通常は非常に効率的ですが、頻繁に呼び出される関数で発生すると、わずかながらパフォーマンスコストが発生する可能性があります。
-
getcallerpc()
の正確性確保:getcallerpc()
は、呼び出し元のプログラムカウンタ(Program Counter: PC)を取得するためのGoランタイム関数です。これはデバッグやプロファイリング、あるいはレース検出器のようなツールが呼び出し元のコンテキストを特定するために使用されます。スタック分割が発生する関数内でgetcallerpc()
が呼び出されると、スタックが拡張された際に、誤ってruntime·lessstack()
という特別なPC値を返す可能性がありました。これは、スタックが不足していることを示す内部的なマーカーであり、実際の呼び出し元のPCではありません。レース検出器のコールバック関数は頻繁に呼び出され、getcallerpc()
を使用する可能性があるため、この問題は検出器の正確性や信頼性に影響を与える可能性がありました。nosplitstack
属性を付与することで、これらの関数内でのスタック分割を防ぎ、getcallerpc()
が常に正しい呼び出し元のPCを返すようにします。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ガベージコレクション、スケジューラ(Goルーチンの管理)、メモリ割り当て、スタック管理などが含まれます。Goプログラムは、オペレーティングシステム上で直接実行されるのではなく、このランタイム上で動作します。
Goレース検出器 (Go Race Detector)
Goレース検出器は、Goプログラムにおけるデータ競合を検出するためのツールです。データ競合は、複数のGoルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。レース検出器は、プログラムの実行中にこれらの競合を特定し、開発者に警告することで、並行処理のバグを早期に発見するのに役立ちます。これは、コンパイル時に特別な計測コードを挿入することで機能します。
スタック分割 (Stack Splitting)
Goルーチンは、最初は非常に小さなスタック(数KB)で開始されます。プログラムの実行中に、関数呼び出しがスタックを使い果たしそうになると、Goランタイムは自動的にスタックを拡張し、より大きなメモリ領域を割り当てます。このプロセスを「スタック分割」と呼びます。これにより、Goルーチンは必要に応じてスタックサイズを動的に調整でき、メモリ効率が向上します。スタック分割は透過的に行われますが、その過程でわずかなオーバーヘッドが発生します。
getcallerpc()
getcallerpc()
は、Goランタイム内部で使用される関数で、現在の関数の呼び出し元のプログラムカウンタ(PC)を取得します。これは、スタックトレースの生成、デバッグ情報の収集、プロファイリング、そしてレース検出器のようなツールが呼び出し元のコンテキストを特定するために利用されます。
runtime·lessstack()
runtime·lessstack()
は、Goランタイムの内部的なマーカーであり、スタックが不足していることを示す特別なプログラムカウンタ値です。通常、getcallerpc()
は実際の呼び出し元のPCを返しますが、スタック分割の途中で呼び出された場合など、特定の状況下ではこのruntime·lessstack()
を返すことがありました。これは、呼び出し元のPCを正確に特定できないことを意味し、レース検出器のようなツールにとっては問題となります。
pragma textflag 7
(nosplitstack)
Goコンパイラには、特定の関数に対して特別な属性を付与するためのpragma
ディレクティブがあります。#pragma textflag 7
は、Goの内部的なコンパイラディレクティブであり、関数がスタック分割を行わないように指示します。これは、その関数が呼び出されたときに、Goランタイムがスタックの拡張を試みないことを意味します。このような関数は「nosplitstack」関数と呼ばれます。通常、これは非常に短い関数や、スタック分割のオーバーヘッドが許容できないパフォーマンスクリティカルな関数に適用されます。
技術的詳細
このコミットの技術的な核心は、Goコンパイラの#pragma textflag 7
ディレクティブを使用して、レース検出器のコールバック関数をnosplitstack
としてマークすることです。
Goの関数呼び出しでは、呼び出し元が呼び出し先のスタックフレームを確保する前に、呼び出し先の関数がスタックを使い切らないか(スタックオーバーフローしないか)をチェックする「スタックチェック」が行われます。もしスタックが不足していると判断された場合、ランタイムはスタックを拡張するためのルーチン(morestack
)を呼び出します。このプロセスがスタック分割です。
レース検出器のコールバック関数(runtime·racewrite
, runtime·raceread
, runtime·racefuncenter
, runtime·racefuncexit
)は、プログラムの実行中に非常に頻繁に呼び出されます。これらの関数が呼び出されるたびにスタックチェックと潜在的なスタック分割が発生すると、そのオーバーヘッドがレース検出器全体のパフォーマンスに影響を与えます。
#pragma textflag 7
をこれらの関数に適用することで、コンパイラはこれらの関数がスタックチェックを行わないようにします。これにより、これらの関数が呼び出される際のスタック分割のオーバーヘッドが排除され、レース検出器の実行速度が向上します。
さらに重要なのは、getcallerpc()
の挙動に対する影響です。スタック分割のプロセス中、特にmorestack
ルーチンが実行されている最中にgetcallerpc()
が呼び出されると、スタックフレームがまだ完全に設定されていないため、正しい呼び出し元のPCを取得できず、代わりにruntime·lessstack()
を返す可能性がありました。レース検出器のコールバック関数は、データ競合のコンテキストを正確に特定するためにgetcallerpc()
を使用する場合があります。これらの関数をnosplitstack
にすることで、関数内でスタック分割が発生しなくなり、getcallerpc()
が常に正確な呼び出し元のPCを返すことが保証されます。これにより、レース検出器の信頼性と診断能力が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/race.c
ファイルに対して行われています。
diff --git a/src/pkg/runtime/race.c b/src/pkg/runtime/race.c
index bea16cc832..ef7eec2b6b 100644
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -47,6 +47,8 @@ runtime·racefini(void)\n }\n \n // Called from instrumented code.\n+// If we split stack, getcallerpc() can return runtime·lessstack().\n+#pragma textflag 7\n void\n runtime·racewrite(uintptr addr)\n {\n@@ -58,6 +60,8 @@ runtime·racewrite(uintptr addr)\n }\n \n // Called from instrumented code.\n+// If we split stack, getcallerpc() can return runtime·lessstack().\n+#pragma textflag 7\n void\n runtime·raceread(uintptr addr)\n {\n@@ -69,6 +73,7 @@ runtime·raceread(uintptr addr)\n }\n \n // Called from instrumented code.\n+#pragma textflag 7\n void\n runtime·racefuncenter(uintptr pc)\n {\n@@ -83,6 +88,7 @@ runtime·racefuncexit(void)\n }\n \n // Called from instrumented code.\n+#pragma textflag 7\n void\n runtime·racefuncexit(void)\n {\n```
## コアとなるコードの解説
上記の差分が示すように、以下の4つの関数定義の直前に`#pragma textflag 7`が追加されています。
1. `runtime·racewrite(uintptr addr)`: メモリへの書き込み操作が計測されたときに呼び出されるコールバック。
2. `runtime·raceread(uintptr addr)`: メモリからの読み込み操作が計測されたときに呼び出されるコールバック。
3. `runtime·racefuncenter(uintptr pc)`: 関数に入ったときに呼び出されるコールバック。
4. `runtime·racefuncexit(void)`: 関数から出るときに呼び出されるコールバック。
これらの関数は、Goのレース検出器が有効になっている場合に、コンパイルされたGoプログラムの各メモリアクセスや関数呼び出しの前後で自動的に挿入される計測コードから呼び出されます。したがって、これらの関数は非常に頻繁に実行される可能性があり、そのパフォーマンスはレース検出器全体の効率に直結します。
`#pragma textflag 7`を付与することで、これらの関数は「nosplitstack」関数として扱われます。これにより、これらの関数が呼び出された際にスタックチェックがスキップされ、スタック分割のオーバーヘッドがなくなります。また、スタック分割が発生しないため、これらの関数内で`getcallerpc()`が呼び出されても、`runtime·lessstack()`のような不正な値が返されることがなくなり、常に正確な呼び出し元のPCが取得できるようになります。これは、レース検出器が正確な競合レポートを生成するために不可欠です。
## 関連リンク
- Go Race Detector: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)
- Go Runtime Source Code (race.c): [https://github.com/golang/go/blob/master/src/runtime/race.go](https://github.com/golang/go/blob/master/src/runtime/race.go) (Note: The file path has changed from `src/pkg/runtime/race.c` to `src/runtime/race.go` in newer Go versions, but the underlying concepts remain similar.)
- Go CL 6812091: [https://golang.org/cl/6812091](https://golang.org/cl/6812091) (Original change list for this commit)
## 参考にした情報源リンク
- Goのスタック分割に関する議論やドキュメント(Goの公式ドキュメントやブログ、Goのソースコードコメントなど)
- Goコンパイラの`pragma`ディレクティブに関する情報(Goのソースコードやコンパイラのドキュメント)
- `getcallerpc()`の挙動に関する情報(Goのソースコードや関連するGoのIssue/CL)
- Go Race Detectorの内部動作に関する情報
- Stack Overflowや技術ブログでのGoランタイム、スタック、レース検出器に関する解説記事