[インデックス 17602] ファイルの概要
このコミットは、GoランタイムにおけるCPUプロファイリングのWindows環境での問題を修正するものです。具体的には、プロファイリング中にゴルーチン(gp
)が現在のM(m->curg
)と一致するかどうかをチェックする条件が、Windowsでは適切ではないために発生していた問題を解決します。
コミット
commit 6d68fc8eeaf32375eda7208b62cedf6ee5d241d0
Author: Russ Cox <rsc@golang.org>
Date: Sun Sep 15 12:05:24 2013 -0400
runtime: fix CPU profiling on Windows
The test 'gp == m->curg' is not valid on Windows,
because the goroutine being profiled is not from the
current m.
TBR=golang-dev
CC=golang-dev
https://golang.org/cl/13718043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6d68fc8eeaf32375eda7208b62cedf6ee5d241d0
元コミット内容
runtime: fix CPU profiling on Windows
The test 'gp == m->curg' is not valid on Windows,
because the goroutine being profiled is not from the
current m.
TBR=golang-dev
CC=golang-dev
https://golang.org/cl/13718043
変更の背景
GoのCPUプロファイリングは、プログラムの実行中にCPUがどの関数に時間を費やしているかを特定するための重要なツールです。プロファイリングは、定期的に実行中のゴルーチンのスタックトレースをサンプリングすることで機能します。このサンプリングは、シグナルハンドラ(Unix系システムの場合)や、WindowsのようなOS固有のメカニズムを通じて行われます。
問題は、Windows環境でのCPUプロファイリングの際に発生していました。Goランタイムでは、プロファイリングの対象となるゴルーチン(gp
)が、現在実行中のM(m->curg
、MはOSスレッドを表すGoランタイムの抽象化)と一致するかどうかを確認する条件 gp == m->curg
が存在しました。この条件は、Unix系システムでは通常有効ですが、Windowsではプロファイリングのメカニズムが異なるため、常に有効ではありませんでした。
Windowsでは、プロファイリングを行うMが、必ずしも現在プロファイリング対象となっているゴルーチンを実行しているMであるとは限りません。os_windows.c
内の profilem
関数が、複数のゴルーチンからレポートを収集する役割を担っているため、gp == m->curg
のチェックが誤った結果を招き、CPUプロファイリングが正しく機能しない可能性がありました。このコミットは、このWindows固有の動作を考慮し、プロファイリングの正確性を確保するために行われました。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ガベージコレクション、スケジューリング、メモリ管理、ゴルーチン管理など、低レベルの操作を担当します。
- ゴルーチン (Goroutine): Goにおける軽量な実行スレッドです。OSスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行できます。Goランタイムのスケジューラによって管理されます。
- M (Machine): Goランタイムの内部概念で、OSスレッドを表します。P (Processor) とG (Goroutine) とともに、Goスケジューラの主要な要素です。MはOSスレッド上でゴルーチンを実行します。
- P (Processor): Goランタイムの内部概念で、論理的なプロセッサを表します。Mとゴルーチンを関連付け、並行実行を可能にします。
- CPUプロファイリング (CPU Profiling): プログラムがCPU時間をどこで消費しているかを分析する手法です。これにより、パフォーマンスのボトルネックを特定し、最適化のヒントを得ることができます。Goでは、
pprof
ツールを使用してCPUプロファイルを取得できます。 runtime·sigprof
: Goランタイム内の関数で、プロファイリングシグナル(またはそれに相当するイベント)が受信されたときに呼び出されます。この関数は、現在の実行コンテキスト(PC: プログラムカウンタ、SP: スタックポインタ、LR: リンクレジスタ、gp: 現在のゴルーチン)を分析し、プロファイリングデータを収集します。gp
(Goroutine Pointer):runtime·sigprof
関数に渡される引数で、プロファイリング対象のゴルーチンへのポインタです。m->curg
: 現在のM(OSスレッド)が実行しているゴルーチンへのポインタです。StackGuard
: スタックオーバーフローを検出するためのガードページです。スタックがこのガードページに到達すると、ランタイムはスタックの拡張を試みます。runtime·gogo
: Goランタイムの内部関数で、ゴルーチンのコンテキストスイッチ(実行状態の切り替え)を行うアセンブリコードです。プロファイリング中にこの関数内にPCがある場合、それはユーザーコードではなくランタイムの内部処理中であるため、プロファイリングの対象外とされます。TBR=golang-dev
/CC=golang-dev
: Goプロジェクトにおけるコードレビューの慣習です。TBR
(To Be Reviewed) はレビュー担当者を示し、CC
(Carbon Copy) は追加の通知先を示します。どちらもgolang-dev
メーリングリストを指しており、Go開発チーム全体がレビューに関与することを示唆しています。https://golang.org/cl/13718043
: GoプロジェクトのコードレビューシステムであるGerritの変更リスト(Change-List)へのリンクです。このリンクから、このコミットに至るまでの議論やレビューの履歴を確認できます。
技術的詳細
GoランタイムのCPUプロファイリングは、特定の時間間隔で実行中のゴルーチンのスタックトレースをサンプリングすることで機能します。このサンプリングは、OSが提供するメカニズム(例えば、Unix系システムではSIGPROF
シグナル、WindowsではSetTimer
やCreateTimerQueueTimer
などのタイマーAPIとスレッドサスペンド/レジューム)を利用して行われます。
src/pkg/runtime/proc.c
内の runtime·sigprof
関数は、プロファイリングイベントが発生した際に呼び出される主要な関数です。この関数は、プロファイリング対象のゴルーチン gp
、プログラムカウンタ pc
、スタックポインタ sp
などの情報を受け取ります。
元のコードでは、プロファイリング対象のゴルーチンが有効な状態であるか、およびプロファイリングに適しているかを判断するために、以下のような条件チェックを行っていました。
if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
traceback = false;
この条件のうち、gp != m->curg
の部分がWindows環境で問題を引き起こしていました。
gp == nil
: ゴルーチンポインタがnil
でないことを確認します。gp != m->curg
: プロファイリング対象のゴルーチンが、現在このM(OSスレッド)で実行されているゴルーチンと一致しない場合。これは、通常、プロファイリング対象が現在のMで実行されているゴルーチンであるべきという前提に基づいています。(uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp
: スタックポインタsp
がゴルーチンgp
のスタック範囲内にあることを確認します。スタックがオーバーフローしていないか、または無効なスタックポインタでないかをチェックします。((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes)
: プログラムカウンタpc
がruntime·gogo
関数(ゴルーチンのコンテキストスイッチを行うアセンブリコード)の範囲内にある場合。この場合、プロファイリングはユーザーコードではなくランタイムの内部処理を捕捉しているため、トレースバックは無効とされます。
Windowsでは、プロファイリングのサンプリングメカニズムがUnix系システムとは異なります。os_windows.c
の profilem
関数は、特定のMが他のMで実行されているゴルーチンを含む、システム内のすべてのユーザーゴルーチンに関するプロファイリングレポートを収集する役割を担っています。つまり、runtime·sigprof
が呼び出されたときに gp
が指すゴルーチンが、必ずしも m->curg
(runtime·sigprof
を呼び出したMが現在実行しているゴルーチン)と一致するとは限らないのです。
このため、gp != m->curg
という条件はWindowsでは誤った traceback = false
を引き起こし、結果としてCPUプロファイリングデータが欠落したり、不正確になったりする原因となっていました。
このコミットは、このWindows固有の動作を考慮し、gp != m->curg
のチェックをWindows環境ではスキップするように変更しました。これにより、Windows上でもCPUプロファイリングが正確に機能するようになります。
コアとなるコードの変更箇所
src/pkg/runtime/proc.c
ファイルの runtime·sigprof
関数内の条件式が変更されました。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2115,7 +2115,13 @@ runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
// To recap, there are no constraints on the assembly being used for the
// transition. We simply require that g and SP match and that the PC is not
// in runtime.gogo.
- if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
+ //
+ // On Windows, one m is sending reports about all the g's, so gp == m->curg
+ // is not a useful comparison. The profilem function in os_windows.c has
+ // already checked that gp is a user g.
+ if(gp == nil ||
+ (!Windows && gp != m->curg) ||
+ (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
((uint8*)runtime·gogo <= pc && pc < (uint8*)runtime·gogo + RuntimeGogoBytes))
traceback = false;
コアとなるコードの解説
変更された行は以下の通りです。
- if(gp == nil || gp != m->curg || (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
+ if(gp == nil ||
+ (!Windows && gp != m->curg) ||
+ (uintptr)sp < gp->stackguard - StackGuard || gp->stackbase < (uintptr)sp ||
元のコードでは、gp != m->curg
という条件が常に評価されていました。
変更後では、この条件が (!Windows && gp != m->curg)
となっています。
!Windows
: これはプリプロセッサディレクティブまたはコンパイル時の定義によって評価される条件で、コンパイルターゲットがWindowsでない場合に真となります。&& gp != m->curg
:!Windows
が真の場合にのみ、gp != m->curg
のチェックが実行されます。
この変更により、Windows環境でGoランタイムがコンパイルされる際には、gp != m->curg
の条件がスキップされるようになります。これにより、Windowsのプロファイリングメカニズムの特性(profilem
関数が複数のゴルーチンからのレポートを処理するため、gp
が必ずしも現在のMのゴルーチンと一致しない)が考慮され、誤ったプロファイリングデータの破棄が防がれます。
コメントも追加されており、この変更の理由が明確に説明されています。
// On Windows, one m is sending reports about all the g's, so gp == m->curg
// is not a useful comparison. The profilem function in os_windows.c has
// already checked that gp is a user g.
このコメントは、Windowsでは1つのMがすべてのゴルーチンに関するレポートを送信するため、gp == m->curg
の比較が有用ではないこと、そして os_windows.c
の profilem
関数が既に gp
がユーザーゴルーチンであることを確認していることを説明しています。これにより、コードの意図が明確になり、将来のメンテナンス性が向上します。
関連リンク
- Go CPUプロファイリングの公式ドキュメント: https://go.dev/doc/diagnose-cpu-profile
- Goのスケジューラに関する詳細(M, P, Gの概念): https://go.dev/doc/articles/go_scheduler.html
参考にした情報源リンク
- Goのコミット履歴 (Gerrit): https://go.googlesource.com/go/+log
- Goのソースコード (GitHubミラー): https://github.com/golang/go
- GoのIssueトラッカー: https://go.dev/issue
- Go開発者メーリングリスト: https://groups.google.com/g/golang-dev
- Goのプロファイリングに関するブログ記事やチュートリアル (一般的な情報源)