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

[インデックス 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プロファイリング機能における複数の既存のバグと制限に対処するために導入されました。主な問題点は以下の通りです。

  1. プロファイリングシグナルの欠落: 特にシステムコール中やCGO(C言語との相互運用)呼び出し中に、プロファイリングシグナルが適切に処理されず、プロファイリングデータが失われる可能性がありました。これは、プロファイリングがGoランタイムの内部状態(特にmcacheの有無)に依存していたためです。
  2. Windows環境でのプロファイリングの不完全性: Windows上でのCPUプロファイリングは、他のOSと比較して信頼性が低く、シグナルが失われたり、CGOプログラムが正しくプロファイリングされないという問題がありました。
  3. Windows CGOプログラムにおけるスレッド設定の不備: Windows上のCGOプログラムでは、GoランタイムのM(Machine)構造体に関連付けられたOSスレッドハンドル(m->thread)が適切に設定されていませんでした。これにより、プロファイリングが正しく機能しない原因となっていました。
  4. Windows CGOプログラムにおけるTLS(Thread Local Storage)設定の不備: CGOスレッドがGoランタイムのTLSを適切に初期化していなかったため、Goルーチン(G)やMなどの重要なランタイム情報にアクセスできず、プロファイリングやその他のランタイム機能に問題が生じていました。
  5. _beginthreadの戻り値チェックの欠如: Windowsで新しいOSスレッドを作成する際に使用される_beginthread関数の戻り値がチェックされていなかったため、スレッド作成に失敗した場合に適切なエラーハンドリングが行われていませんでした。

これらの問題は、Goプログラムのパフォーマンス分析を困難にし、特にWindows環境やCGOを使用するアプリケーションにおいて、正確なプロファイリングデータを得ることを妨げていました。コミットメッセージに記載されているFixes #6417Fixes #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の特性に関する知識が必要です。

  1. 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が利用できない状態になることがあります。
  2. CPUプロファイリングの仕組み:

    • GoのCPUプロファイリングは、通常、OSのシグナル(Unix系ではSIGPROF、Windowsではタイマーベースの割り込み)を利用して行われます。
    • 一定の間隔でシグナルが送られ、そのシグナルハンドラ内で現在実行中のGoroutineのスタックトレースを収集します。これにより、CPU時間の大部分を消費している関数を特定できます。
    • プロファイリングの精度は、シグナルの頻度(prof.hz)に依存します。
  3. CGO (C Foreign Function Interface):

    • GoプログラムからC言語の関数を呼び出すためのメカニズムです。
    • CGO呼び出しが行われると、Goランタイムは現在のGoroutineをOSスレッド(M)から切り離し、Cコードが実行される間、OSスレッドはGoランタイムの管理外で動作します。CコードがGoに制御を戻すまで、Goランタイムはプロファイリングシグナルを処理できない可能性があります。
    • CGOスレッドは、Goランタイムが直接管理するMとは異なる方法でOSスレッドが作成されるため、Goランタイムの内部状態(特にTLS)が適切に設定されていない場合があります。
  4. 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)。
  5. CONTEXT構造体とGetThreadContext:

    • Windows APIのCONTEXT構造体は、特定のスレッドのCPUレジスタの状態(プログラムカウンタ、スタックポインタなど)を保持します。
    • GetThreadContext関数は、指定されたスレッドの現在のコンテキストを取得するために使用されます。CPUプロファイリングでは、これにより実行中のスレッドのPC(プログラムカウンタ)とSP(スタックポインタ)を取得し、スタックトレースを生成します。

技術的詳細

このコミットは、GoランタイムのCPUプロファイリングの堅牢性を向上させるために、複数の技術的な変更を導入しています。

  1. mcacheの有無に関わらないプロファイリングシグナル処理の改善:

    • 以前は、runtime·sigprof関数内でm == nil || m->mcache == nilの場合にスタックトレースの収集(traceback)をスキップしていました。これは、システムコール中やCGO呼び出し中など、Mがmcacheを持たない状態にある場合にプロファイリングデータが失われる原因となっていました。
    • このコミットでは、runtime·sigprofの引数にM *mp(現在のM)を追加し、mcacheのチェックを削除しました。代わりに、プロファイリング処理の開始時にmp->mcacheを一時的にnilに設定し、処理終了後に元に戻すことで、プロファイリング中にメモリ割り当てが行われないようにしつつ、mcacheの有無に依存しないプロファイリングを可能にしています。これにより、mcacheがない状態でもプロファイリングシグナルが失われることを防ぎます。
  2. Windows CGOプログラムにおけるm->threadの適切な設定:

    • 以前のGoランタイムでは、CGOによって作成されたスレッド(_beginthread経由)は、GoランタイムのM構造体内のthreadフィールド(OSスレッドハンドル)が適切に設定されていませんでした。runtime·newosproc関数内でmp->threadへのthandleatomicstorepが、runtime·minit関数に移動されました。
    • このコミットでは、runtime·minit関数(Mの初期化時に呼び出される)内で、現在のOSスレッドのハンドルをDuplicateHandleを使って取得し、m->threadに格納するように変更されました。これにより、CGOスレッドを含むすべてのMが有効なOSスレッドハンドルを持つようになり、GetThreadContextなどのOSスレッドハンドルを必要とするプロファイリング関連の操作が正しく機能するようになります。
  3. 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などの情報にアクセスできるようになり、プロファイリングが正しく機能するようになります。
  4. _beginthreadの戻り値チェック:

    • _beginthread関数の戻り値がuintptr_t thandle;に格納され、if(thandle == -1)でチェックされるようになりました。
    • スレッド作成に失敗した場合(戻り値が-1)、fprintf(stderr, "runtime: failed to create new OS thread (%d)\\n", errno);でエラーメッセージを出力し、abort();を呼び出してプログラムを異常終了させるようになりました。これにより、スレッド作成の失敗がサイレントに無視されることを防ぎ、デバッグが容易になります。
  5. runtime·sigprof関数のシグネチャ変更:

    • runtime·sigprof関数は、以前はG *gp(Goroutineポインタ)のみを受け取っていましたが、M *mp(Machineポインタ)も受け取るようにシグネチャが変更されました。
    • これにより、runtime·sigprof内で現在のMの情報を直接利用できるようになり、特にWindowsのように単一のMが複数のGのプロファイリングレポートを送信するようなシナリオで、より正確なプロファイリングが可能になります。
  6. テストコードの修正:

    • src/pkg/runtime/pprof/pprof_test.goから、Windows環境でのプロファイリングの不安定さやCGOプログラムでの問題に対応するためのスキップロジックが削除されました。これは、上記の修正によってこれらの問題が解決されたことを示しています。

これらの変更は、GoランタイムのCPUプロファイリングの信頼性と正確性を大幅に向上させ、特にWindows環境やCGOを使用するアプリケーションでのプロファイリングの課題を解決しています。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

  1. src/pkg/runtime/cgo/gcc_windows_386.c および src/pkg/runtime/cgo/gcc_windows_amd64.c:

    • _beginthreadの戻り値チェックが追加されました。
    • threadentry関数内で、TLSの初期化方法が変更され、ts.tlsを使用するように修正されました。以前のLocalAllocLocalFreeが削除されました。
    • libcgo.hThreadStart構造体に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); // 削除
    }
    
  2. src/pkg/runtime/cgo/libcgo.h:

    • ThreadStart構造体にuintptr *tls;フィールドが追加されました。
    struct ThreadStart
    {
    	uintptr m;
    	G *g;
    	uintptr *tls; // 追加
    	void (*fn)(void);
    };
    
  3. 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); // 変更後
    // ...
    
  4. 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); // 変更後
    }
    
  5. 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; // 追加
    }
    
  6. 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); // 変更後
    
  7. 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など)
    
  8. 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プログラムにおけるその堅牢性を高めることにあります。

  1. runtime·sigprof関数の役割と変更:

    • runtime·sigprofは、CPUプロファイリングシグナル(SIGPROFなど)が受信されたときに呼び出される主要な関数です。この関数は、シグナル発生時のプログラムカウンタ(PC)とスタックポインタ(SP)を取得し、現在のGoroutineのスタックトレースを収集してプロファイリングデータとして記録します。
    • 変更前: この関数は、グローバルなm変数(現在のM)に依存しており、特にm->mcachenilの場合(システムコール中など)にはスタックトレースの収集をスキップしていました。また、Windowsでは単一のMが複数のGのプロファイリングレポートを送信する特殊な状況があり、グローバルなmに依存することが問題でした。
    • 変更後:
      • runtime·sigprofは、引数としてM *mpを受け取るようになりました。これにより、グローバルなmに依存せず、プロファイリング対象のMの情報を直接利用できるようになりました。
      • mcacheの有無によるスキップロジックが削除されました。代わりに、プロファイリング処理の開始時にmp->mcacheを一時的にnilに設定し、処理終了後に元に戻すことで、プロファイリング中にメモリ割り当てが行われないようにしつつ、mcacheの状態に左右されずにプロファイリングを続行できるようにしました。これは、プロファイリングがGCと並行して実行されるため、割り当てを行わないという制約を満たしつつ、より多くのシナリオでプロファイリングを可能にするための重要な変更です。
      • gp != m->curgのチェックがgp != mp->curgに変更され、現在のMが管理するGoroutineとの比較がより正確になりました。
  2. 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のソースコード (特にsrc/pkg/runtimeディレクトリ)
  • GoのIssueトラッカー (上記関連リンク)
  • Goのドキュメント (CPUプロファイリング、CGOに関するセクション)
  • Windows APIドキュメント (_beginthread, DuplicateHandle, GetThreadContext, CONTEXT構造体, TLSに関する情報)
  • Goのランタイムスケジューラに関する一般的な解説記事 (M, G, Pモデル)