[インデックス 18322] ファイルの概要
コミット
このコミット 8a3c587dc1a5f7a9cd87b764b74e28a57935ab40
は、GoランタイムにおけるCPUプロファイリングの修正と改善を目的としています。特に、プロファイリングシグナルが失われる問題、Windows環境でのプロファイリングの不具合、およびCGOプログラムにおけるスレッドローカルストレージ (TLS) の設定に関する問題に対処しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8a3c587dc1a5f7a9cd87b764b74e28a57935ab40
元コミット内容
runtime: fix and improve CPU profiling
- do not lose profiling signals when we have no mcache (possible for syscalls/cgo)
- do not lose any profiling signals on windows
- fix profiling of cgo programs on windows (they had no m->thread setup)
- properly setup tls in cgo programs on windows
- check _beginthread return value
Fixes #6417.
Fixes #6986.
R=alex.brainman, rsc
CC=golang-codereviews
https://golang.org/cl/44820047
変更の背景
このコミットは、GoのCPUプロファイリング機能における複数の既存のバグと制限に対処するために導入されました。主な問題点は以下の通りです。
- プロファイリングシグナルの欠落: 特にシステムコール中やCGO(C言語との相互運用)呼び出し中に、プロファイリングシグナルが適切に処理されず、プロファイリングデータが失われる可能性がありました。これは、プロファイリングがGoランタイムの内部状態(特に
mcache
の有無)に依存していたためです。 - Windows環境でのプロファイリングの不完全性: Windows上でのCPUプロファイリングは、他のOSと比較して信頼性が低く、シグナルが失われたり、CGOプログラムが正しくプロファイリングされないという問題がありました。
- Windows CGOプログラムにおけるスレッド設定の不備: Windows上のCGOプログラムでは、GoランタイムのM(Machine)構造体に関連付けられたOSスレッドハンドル(
m->thread
)が適切に設定されていませんでした。これにより、プロファイリングが正しく機能しない原因となっていました。 - Windows CGOプログラムにおけるTLS(Thread Local Storage)設定の不備: CGOスレッドがGoランタイムのTLSを適切に初期化していなかったため、Goルーチン(G)やMなどの重要なランタイム情報にアクセスできず、プロファイリングやその他のランタイム機能に問題が生じていました。
_beginthread
の戻り値チェックの欠如: Windowsで新しいOSスレッドを作成する際に使用される_beginthread
関数の戻り値がチェックされていなかったため、スレッド作成に失敗した場合に適切なエラーハンドリングが行われていませんでした。
これらの問題は、Goプログラムのパフォーマンス分析を困難にし、特にWindows環境やCGOを使用するアプリケーションにおいて、正確なプロファイリングデータを得ることを妨げていました。コミットメッセージに記載されているFixes #6417
とFixes #6986
は、これらの具体的なバグ報告に対応していることを示しています。
- Issue #6417: "runtime: cpu profiler is flaky on windows" (WindowsでのCPUプロファイラが不安定)
- Issue #6986: "runtime: cpu profiler does not work for cgo programs on windows/amd64" (Windows/amd64上のCGOプログラムでCPUプロファイラが動作しない)
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムの概念とWindows OSの特性に関する知識が必要です。
-
GoランタイムのM, G, Pモデル:
- G (Goroutine): Goにおける軽量な実行単位。ユーザーが書くGoコードはGoroutineとして実行されます。
- M (Machine): OSスレッドを表します。Goランタイムは、OSスレッドを抽象化し、その上でGoroutineを実行します。各Mは、OSスレッドのコンテキスト(スタック、レジスタなど)を管理します。
- P (Processor): 論理プロセッサを表します。MとGの間の仲介役となり、Goroutineの実行をスケジュールします。Pは、実行可能なGoroutineのキューを保持し、MにGoroutineをディスパッチします。
mcache
: 各Mに紐付けられたローカルなメモリキャッシュです。Goroutineのスタック割り当てなど、頻繁に発生するメモリ操作のパフォーマンスを向上させます。システムコール中やCGO呼び出し中は、MがOSスレッドをブロックするため、mcache
が利用できない状態になることがあります。
-
CPUプロファイリングの仕組み:
- GoのCPUプロファイリングは、通常、OSのシグナル(Unix系では
SIGPROF
、Windowsではタイマーベースの割り込み)を利用して行われます。 - 一定の間隔でシグナルが送られ、そのシグナルハンドラ内で現在実行中のGoroutineのスタックトレースを収集します。これにより、CPU時間の大部分を消費している関数を特定できます。
- プロファイリングの精度は、シグナルの頻度(
prof.hz
)に依存します。
- GoのCPUプロファイリングは、通常、OSのシグナル(Unix系では
-
CGO (C Foreign Function Interface):
- GoプログラムからC言語の関数を呼び出すためのメカニズムです。
- CGO呼び出しが行われると、Goランタイムは現在のGoroutineをOSスレッド(M)から切り離し、Cコードが実行される間、OSスレッドはGoランタイムの管理外で動作します。CコードがGoに制御を戻すまで、Goランタイムはプロファイリングシグナルを処理できない可能性があります。
- CGOスレッドは、Goランタイムが直接管理するMとは異なる方法でOSスレッドが作成されるため、Goランタイムの内部状態(特にTLS)が適切に設定されていない場合があります。
-
WindowsにおけるスレッドとTLS (Thread Local Storage):
_beginthread
: Windowsで新しいスレッドを作成するためのCランタイムライブラリ関数です。Goランタイムは、CGOスレッドの作成にこれを使用することがあります。DuplicateHandle
: 既存のオブジェクトハンドル(この場合はスレッドハンドル)を複製するWindows API関数です。Goランタイムは、現在のOSスレッドのハンドルを取得するためにこれを使用します。- TLS (Thread Local Storage): 各スレッドが独自のデータを持つことができるメカニズムです。Goランタイムは、現在のGoroutine(G)やMなどの重要な情報をTLSに保存し、スレッドがこれらの情報に迅速にアクセスできるようにします。Windowsでは、
FS
またはGS
セグメントレジスタを介してTLSにアクセスすることが一般的です(32-bitではFS
、64-bitではGS
)。
-
CONTEXT
構造体とGetThreadContext
:- Windows APIの
CONTEXT
構造体は、特定のスレッドのCPUレジスタの状態(プログラムカウンタ、スタックポインタなど)を保持します。 GetThreadContext
関数は、指定されたスレッドの現在のコンテキストを取得するために使用されます。CPUプロファイリングでは、これにより実行中のスレッドのPC(プログラムカウンタ)とSP(スタックポインタ)を取得し、スタックトレースを生成します。
- Windows APIの
技術的詳細
このコミットは、GoランタイムのCPUプロファイリングの堅牢性を向上させるために、複数の技術的な変更を導入しています。
-
mcache
の有無に関わらないプロファイリングシグナル処理の改善:- 以前は、
runtime·sigprof
関数内でm == nil || m->mcache == nil
の場合にスタックトレースの収集(traceback
)をスキップしていました。これは、システムコール中やCGO呼び出し中など、Mがmcache
を持たない状態にある場合にプロファイリングデータが失われる原因となっていました。 - このコミットでは、
runtime·sigprof
の引数にM *mp
(現在のM)を追加し、mcache
のチェックを削除しました。代わりに、プロファイリング処理の開始時にmp->mcache
を一時的にnil
に設定し、処理終了後に元に戻すことで、プロファイリング中にメモリ割り当てが行われないようにしつつ、mcache
の有無に依存しないプロファイリングを可能にしています。これにより、mcache
がない状態でもプロファイリングシグナルが失われることを防ぎます。
- 以前は、
-
Windows CGOプログラムにおける
m->thread
の適切な設定:- 以前のGoランタイムでは、CGOによって作成されたスレッド(
_beginthread
経由)は、GoランタイムのM構造体内のthread
フィールド(OSスレッドハンドル)が適切に設定されていませんでした。runtime·newosproc
関数内でmp->thread
へのthandle
のatomicstorep
が、runtime·minit
関数に移動されました。 - このコミットでは、
runtime·minit
関数(Mの初期化時に呼び出される)内で、現在のOSスレッドのハンドルをDuplicateHandle
を使って取得し、m->thread
に格納するように変更されました。これにより、CGOスレッドを含むすべてのMが有効なOSスレッドハンドルを持つようになり、GetThreadContext
などのOSスレッドハンドルを必要とするプロファイリング関連の操作が正しく機能するようになります。
- 以前のGoランタイムでは、CGOによって作成されたスレッド(
-
Windows CGOプログラムにおけるTLSの適切な設定:
- CGOスレッドがGoランタイムのTLSを適切に初期化していなかった問題に対し、
ThreadStart
構造体にuintptr *tls
フィールドが追加されました。 _cgo_sys_thread_start
を呼び出す際に、ts.tls = mp->tls;
として、Goランタイムが管理するTLSポインタをCGOスレッドに渡すように変更されました。- CGOの
threadentry
関数(CGOスレッドのエントリポイント)では、以前はLocalAlloc
で新しいTLS領域を割り当てていましたが、この変更により、Goランタイムから渡されたts.tls
を直接使用するように修正されました。これにより、CGOスレッドがGoランタイムのGやMなどの情報にアクセスできるようになり、プロファイリングが正しく機能するようになります。
- CGOスレッドがGoランタイムのTLSを適切に初期化していなかった問題に対し、
-
_beginthread
の戻り値チェック:_beginthread
関数の戻り値がuintptr_t thandle;
に格納され、if(thandle == -1)
でチェックされるようになりました。- スレッド作成に失敗した場合(戻り値が
-1
)、fprintf(stderr, "runtime: failed to create new OS thread (%d)\\n", errno);
でエラーメッセージを出力し、abort();
を呼び出してプログラムを異常終了させるようになりました。これにより、スレッド作成の失敗がサイレントに無視されることを防ぎ、デバッグが容易になります。
-
runtime·sigprof
関数のシグネチャ変更:runtime·sigprof
関数は、以前はG *gp
(Goroutineポインタ)のみを受け取っていましたが、M *mp
(Machineポインタ)も受け取るようにシグネチャが変更されました。- これにより、
runtime·sigprof
内で現在のMの情報を直接利用できるようになり、特にWindowsのように単一のMが複数のGのプロファイリングレポートを送信するようなシナリオで、より正確なプロファイリングが可能になります。
-
テストコードの修正:
src/pkg/runtime/pprof/pprof_test.go
から、Windows環境でのプロファイリングの不安定さやCGOプログラムでの問題に対応するためのスキップロジックが削除されました。これは、上記の修正によってこれらの問題が解決されたことを示しています。
これらの変更は、GoランタイムのCPUプロファイリングの信頼性と正確性を大幅に向上させ、特にWindows環境やCGOを使用するアプリケーションでのプロファイリングの課題を解決しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードスニペットは以下の通りです。
-
src/pkg/runtime/cgo/gcc_windows_386.c
およびsrc/pkg/runtime/cgo/gcc_windows_amd64.c
:_beginthread
の戻り値チェックが追加されました。threadentry
関数内で、TLSの初期化方法が変更され、ts.tls
を使用するように修正されました。以前のLocalAlloc
とLocalFree
が削除されました。libcgo.h
のThreadStart
構造体にuintptr *tls;
が追加されたことに伴い、アセンブリコードのTLSアクセス部分もtls0
からts.tls
を参照するように変更されました。
// gcc_windows_386.c, gcc_windows_amd64.c void _cgo_sys_thread_start(ThreadStart *ts) { uintptr_t thandle; thandle = _beginthread(threadentry, 0, ts); if(thandle == -1) { fprintf(stderr, "runtime: failed to create new OS thread (%d)\\n", errno); abort(); } } static void threadentry(void *v) { ThreadStart ts; // void *tls0; // 削除 ts = *(ThreadStart*)v; free(v); /* * Set specific keys in thread local storage. */ // tls0 = (void*)LocalAlloc(LPTR, 32); // 削除 asm volatile ( // ... // :: "r"(tls0), "r"(ts.g), "r"(ts.m) : "%eax" // 変更前 :: "r"(ts.tls), "r"(ts.g), "r"(ts.m) : "%eax" // 変更後 ); crosscall_386(ts.fn); // LocalFree(tls0); // 削除 }
-
src/pkg/runtime/cgo/libcgo.h
:ThreadStart
構造体にuintptr *tls;
フィールドが追加されました。
struct ThreadStart { uintptr m; G *g; uintptr *tls; // 追加 void (*fn)(void); };
-
src/pkg/runtime/os_windows.c
:runtime·osinit
からm->thread
の初期化ロジックが削除され、runtime·minit
に移動されました。runtime·minit
関数内で、現在のOSスレッドのハンドルをDuplicateHandle
を使って取得し、m->thread
に格納するロジックが追加されました。profilem
関数内で、runtime·dosigprof
の呼び出しにmp
引数が追加されました。runtime·dosigprof
関数のシグネチャが変更され、M *mp
引数を受け取るようになりました。
// runtime·minit void runtime·minit(void) { void *thandle; // -1 = current process, -2 = current thread runtime·stdcall(runtime·DuplicateHandle, 7, (uintptr)-1, (uintptr)-2, (uintptr)-1, &thandle, (uintptr)0, (uintptr)0, (uintptr)DUPLICATE_SAME_ACCESS); runtime·atomicstorep(&m->thread, thandle); runtime·install_exception_handler(); } // profilem // ... // runtime·dosigprof(r, gp); // 変更前 runtime·dosigprof(r, gp, mp); // 変更後 // ...
-
src/pkg/runtime/os_windows_386.c
およびsrc/pkg/runtime/os_windows_amd64.c
:runtime·dosigprof
関数のシグネチャが変更され、M *mp
引数を受け取るようになりました。runtime·sigprof
の呼び出しにmp
引数が追加されました。
// runtime·dosigprof void // runtime·dosigprof(Context *r, G *gp) // 変更前 runtime·dosigprof(Context *r, G *gp, M *mp) // 変更後 { // runtime·sigprof((uint8*)r->Eip, (uint8*)r->Esp, nil, gp); // 変更前 runtime·sigprof((uint8*)r->Eip, (uint8*)r->Esp, nil, gp, mp); // 変更後 }
-
src/pkg/runtime/proc.c
:CgoThreadStart
構造体にuintptr *tls;
フィールドが追加されました。newm
関数内で、CGOスレッドを起動する際にts.tls = mp->tls;
としてTLSポインタを渡すように変更されました。runtime·sigprof
関数のシグネチャが変更され、M *mp
引数を受け取るようになりました。runtime·sigprof
関数内で、mcache
の有無によるtraceback
のスキップロジックが削除され、代わりにmp->mcache
を一時的にnil
に設定するロジックが追加されました。runtime·sigprof
関数内で、gp != m->curg
のチェックがgp != mp->curg
に変更されました。m->racecall
のチェックがmp->racecall
に変更されました。
// CgoThreadStart struct CgoThreadStart { M *m; G *g; uintptr *tls; // 追加 void (*fn)(void); }; // newm // ... ts.g = mp->g0; ts.tls = mp->tls; // 追加 ts.fn = runtime·mstart; runtime·asmcgocall(_cgo_thread_start, &ts); // ... // runtime·sigprof void // runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp) // 変更前 runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp, M *mp) // 変更後 { // ... // if(prof.fn == nil || prof.hz == 0) // return; // traceback = true; // // Windows does profiling in a dedicated thread w/o m. // if(!Windows && (m == nil || m->mcache == nil)) // traceback = false; // 削除 MCache *mcache; // Do not use global m in this function, use mp instead. // On windows one m is sending reports about all the g's, so m means a wrong thing. byte m; m = 0; USED(m); // Profiling runs concurrently with GC, so it must not allocate. mcache = mp->mcache; mp->mcache = nil; // ... // if(gp == nil || // (!Windows && gp != m->curg) || // 変更前 traceback = true; if(gp == nil || gp != mp->curg || // 変更後 // ... // if(m != nil && m->racecall) // 変更前 if(mp != nil && mp->racecall) // 変更後 // ... // if(prof.fn == nil) { // runtime·unlock(&prof); // return; // } // ... // runtime·unlock(&prof); // } if(prof.fn == nil) { runtime·unlock(&prof); mp->mcache = mcache; // 追加 return; } // ... runtime·unlock(&prof); mp->mcache = mcache; // 追加 }
-
src/pkg/runtime/runtime.h
:runtime·sigprof
関数のプロトタイプ宣言が変更され、M *mp
引数が追加されました。
// runtime.h // void runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp); // 変更前 void runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp, M *mp); // 変更後
-
src/pkg/runtime/signal_386.c
,src/pkg/runtime/signal_amd64.c
,src/pkg/runtime/signal_arm.c
:runtime·sigprof
の呼び出しにm
引数が追加されました。
// signal_386.c, signal_amd64.c, signal_arm.c // runtime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp); // 変更前 runtime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp, m); // 変更後 (amd64, armも同様にSIG_RIP, SIG_PCなど)
-
src/pkg/runtime/pprof/pprof_test.go
:- Windows環境でのテストスキップロジックが削除されました。
// TestCPUProfileMultithreaded // // TODO(brainman): delete when issue 6986 is fixed. // if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" { // t.Skip("skipping broken test on windows-amd64-race") // } // 削除 // TestGoroutineSwitch // if runtime.GOOS == "windows" { // t.Skip("flaky test; see http://golang.org/issue/6417") // } // 削除 // TestMathBigDivide // // TODO(brainman): delete when issue 6986 is fixed. // if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" { // t.Skip("skipping broken test on windows-amd64-race") // } // 削除
コアとなるコードの解説
このコミットの核となる変更は、GoランタイムがCPUプロファイリングシグナルを処理する方法、特にWindows環境とCGOプログラムにおけるその堅牢性を高めることにあります。
-
runtime·sigprof
関数の役割と変更:runtime·sigprof
は、CPUプロファイリングシグナル(SIGPROF
など)が受信されたときに呼び出される主要な関数です。この関数は、シグナル発生時のプログラムカウンタ(PC)とスタックポインタ(SP)を取得し、現在のGoroutineのスタックトレースを収集してプロファイリングデータとして記録します。- 変更前: この関数は、グローバルな
m
変数(現在のM)に依存しており、特にm->mcache
がnil
の場合(システムコール中など)にはスタックトレースの収集をスキップしていました。また、Windowsでは単一のMが複数のGのプロファイリングレポートを送信する特殊な状況があり、グローバルなm
に依存することが問題でした。 - 変更後:
runtime·sigprof
は、引数としてM *mp
を受け取るようになりました。これにより、グローバルなm
に依存せず、プロファイリング対象のMの情報を直接利用できるようになりました。mcache
の有無によるスキップロジックが削除されました。代わりに、プロファイリング処理の開始時にmp->mcache
を一時的にnil
に設定し、処理終了後に元に戻すことで、プロファイリング中にメモリ割り当てが行われないようにしつつ、mcache
の状態に左右されずにプロファイリングを続行できるようにしました。これは、プロファイリングがGCと並行して実行されるため、割り当てを行わないという制約を満たしつつ、より多くのシナリオでプロファイリングを可能にするための重要な変更です。gp != m->curg
のチェックがgp != mp->curg
に変更され、現在のMが管理するGoroutineとの比較がより正確になりました。
-
Windows CGOスレッドの初期化とTLS設定:
- CGOプログラムがGoランタイムの外部でOSスレッドを作成する場合、これらのスレッドはGoランタイムの内部状態(特にMとTLS)を適切に初期化する必要があります。
_beginthread
の戻り値チェック: スレッド作成の失敗は、以前は検出されませんでしたが、この変更により、失敗時にエラーメッセージを出力し、プログラムを終了させることで、デバッグと堅牢性が向上しました。m->thread
の初期化:runtime·minit
関数内で、現在のOSスレッドのハンドルをDuplicateHandle
で取得し、m->thread
に格納するように変更されました。これにより、GoランタイムがCGOスレッドを含むすべてのOSスレッドのハンドルを追跡できるようになり、GetThreadContext
などのOSレベルの操作が正しく実行できるようになります。- TLSの適切な設定:
ThreadStart
構造体にtls
ポインタが追加され、Goランタイムが管理するTLS領域へのポインタがCGOスレッドに渡されるようになりました。CGOスレッドのエントリポイントであるthreadentry
関数では、この渡されたtls
ポインタを直接使用して、GoランタイムのGやMなどの情報をTLSに設定します。これにより、CGOスレッドがGoランタイムのコンテキストを正しく認識し、プロファイリングやその他のランタイム機能が期待通りに動作するようになります。
これらの変更は、GoランタイムのCPUプロファイリングが、システムコール中やCGO呼び出し中、およびWindows環境といった複雑なシナリオにおいても、より正確で信頼性の高いデータを提供できるようにするための基盤を強化しています。
関連リンク
- Go Issue #6417: runtime: cpu profiler is flaky on windows
- Go Issue #6986: runtime: cpu profiler does not work for cgo programs on windows/amd64
- Go CL 44820047: https://golang.org/cl/44820047
参考にした情報源リンク
- Goのソースコード (特に
src/pkg/runtime
ディレクトリ) - GoのIssueトラッカー (上記関連リンク)
- Goのドキュメント (CPUプロファイリング、CGOに関するセクション)
- Windows APIドキュメント (
_beginthread
,DuplicateHandle
,GetThreadContext
,CONTEXT
構造体, TLSに関する情報) - Goのランタイムスケジューラに関する一般的な解説記事 (M, G, Pモデル)