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

[インデックス 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ではSetTimerCreateTimerQueueTimerなどのタイマー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): プログラムカウンタ pcruntime·gogo 関数(ゴルーチンのコンテキストスイッチを行うアセンブリコード)の範囲内にある場合。この場合、プロファイリングはユーザーコードではなくランタイムの内部処理を捕捉しているため、トレースバックは無効とされます。

Windowsでは、プロファイリングのサンプリングメカニズムがUnix系システムとは異なります。os_windows.cprofilem 関数は、特定のMが他のMで実行されているゴルーチンを含む、システム内のすべてのユーザーゴルーチンに関するプロファイリングレポートを収集する役割を担っています。つまり、runtime·sigprof が呼び出されたときに gp が指すゴルーチンが、必ずしも m->curgruntime·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.cprofilem 関数が既に gp がユーザーゴルーチンであることを確認していることを説明しています。これにより、コードの意図が明確になり、将来のメンテナンス性が向上します。

関連リンク

参考にした情報源リンク