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

[インデックス 18471] ファイルの概要

このコミットは、GoランタイムのCPUプロファイリング機能の改善を目的としています。特に、ガベージコレクション(GC)、システムコール(syscall)、およびCgo呼び出しに関連するプロファイル情報の精度と有用性を向上させることに焦点を当てています。従来のプロファイリングでは、これらの領域でのCPU使用率が「System->etext」という不明瞭なカテゴリに分類されていましたが、この変更により、より具体的な情報(GCフレームの追加、Goスタックへの置き換え)が提供されるようになります。

コミット

commit 5e72fae9b2c4fddc67a5d8ea0aecf3f73234d83e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Feb 12 22:31:36 2014 +0400

    runtime: improve cpu profiles for GC/syscalls/cgo
    Current "System->etext" is not very informative.
    Add parent "GC" frame.
    Replace un-unwindable syscall/cgo frames with Go stack that leads to the call.
    
    LGTM=rsc
    R=rsc, alex.brainman, ality
    CC=golang-codereviews
    https://golang.org/cl/61270043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5e72fae9b2c4fddc67a5d8ea0aecf3f73234d83e

元コミット内容

このコミットの目的は、GoプログラムのCPUプロファイルが提供する情報の質を高めることです。特に、ガベージコレクション(GC)、システムコール、およびCgo(GoとC言語の相互運用機能)に関連するCPU使用率の報告方法を改善します。

以前のプロファイルでは、これらの活動によって消費されたCPU時間が「System->etext」という一般的なカテゴリにまとめられており、これは具体的なパフォーマンスボトルネックを特定する上で十分な情報を提供していませんでした。

この変更により、以下の改善が図られます。

  1. GCフレームの追加: GC活動中に消費されたCPU時間に対して、明確な「GC」親フレームがプロファイルスタックに追加されます。これにより、GCがどの程度のCPU時間を消費しているかをより正確に把握できるようになります。
  2. システムコール/Cgoフレームの置き換え: システムコールやCgo呼び出しは、Goランタイムの外部で行われるため、通常のGoスタックトレースではその内部を「アンワインド」(スタックを遡って呼び出し元を特定する)することが困難でした。このコミットでは、これらのアンワインド不可能なフレームを、そのシステムコールやCgo呼び出しをトリガーしたGoコードのスタックトレースに置き換えます。これにより、Goコードのどの部分がこれらの外部呼び出しに多くの時間を費やしているかを特定できるようになります。

変更の背景

GoのCPUプロファイラは、プログラムの実行中に定期的にサンプリングを行い、その時点での実行中の関数とその呼び出しスタックを記録することで、CPU時間の消費パターンを可視化します。しかし、Goランタイムの内部処理、特にガベージコレクション、システムコール、およびCgo呼び出しは、通常のGo関数とは異なる実行コンテキストを持つため、プロファイラが正確なスタックトレースを収集するのが困難な場合がありました。

具体的には、以下のような問題がありました。

  • 不明瞭な「System->etext」カテゴリ: GoランタイムがOSのシステムコールを実行している間や、Cgoを介してCコードを実行している間、プロファイラはGoのスタックを正確にアンワインドできず、これらの活動を「System」または「etext」(Goバイナリのコードセグメントの終わりを示すシンボル)という汎用的なカテゴリに分類していました。これは、ユーザーがGoプログラムのパフォーマンスボトルネックを特定する上で、具体的な洞察を得ることを妨げていました。例えば、システムコールに時間がかかっているのか、それともGCに時間がかかっているのかが区別できませんでした。
  • GCの可視性の欠如: GCはGoプログラムのパフォーマンスに大きな影響を与える可能性がありますが、そのCPU使用率が明確にプロファイルに表示されないため、GCの最適化が必要かどうかを判断するのが困難でした。
  • Cgo/システムコールの呼び出し元の不明瞭さ: Cgoやシステムコール自体がCPU時間を消費している場合でも、その呼び出し元のGoコードがプロファイルに表示されないため、どのGoコードがこれらの高コストな外部呼び出しを引き起こしているのかを特定できませんでした。

このコミットは、これらの問題を解決し、プロファイリングデータからより実用的な洞察を得られるようにすることで、Go開発者がパフォーマンスの問題をより効率的に診断・解決できるようにすることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下の概念について基本的な知識が必要です。

  1. CPUプロファイリング:

    • サンプリングプロファイラ: GoのCPUプロファイラはサンプリングベースです。これは、一定の間隔(例: 100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのプログラムカウンタ(PC)と呼び出しスタック(コールスタック)を記録します。
    • コールスタック(呼び出しスタック): プログラムが実行されているときに、現在実行中の関数から、その関数を呼び出した関数、さらにその関数を呼び出した関数へと遡る一連の関数のリストです。プロファイラはこれを使って、どの関数がCPU時間を消費しているかを特定します。
    • スタックアンワインディング: コールスタックを遡って、各関数の呼び出し元を特定するプロセスです。これは、プログラムカウンタとスタックポインタ(SP)の情報を使って行われます。
  2. Goランタイム:

    • M (Machine): OSのスレッドを表すGoランタイムの構造体です。各Mは1つのOSスレッドに対応し、Goルーチン(G)の実行やシステムコール、GCなどのランタイム処理を担当します。
    • G (Goroutine): Goの軽量な実行単位です。Goプログラムの並行性を実現します。
    • P (Processor): Goルーチンを実行するための論理プロセッサです。MとGの間に位置し、GoルーチンをMにディスパッチします。
    • runtime·sigprof: CPUプロファイリングのサンプリングシグナル(通常はSIGPROF)を受け取った際にGoランタイムが呼び出すハンドラ関数です。この関数内でスタックトレースの収集が行われます。
    • runtime·gentraceback: Goのスタックトレースを生成するためのランタイム関数です。PC、SP、Gなどの情報からGoの呼び出しスタックを再構築します。
  3. ガベージコレクション (GC):

    • Goの自動メモリ管理機能です。不要になったメモリを自動的に解放し、メモリリークを防ぎます。GCはプログラムの実行中にバックグラウンドで動作し、CPU時間を消費します。
    • mp->gcing / mp->helpgc: M構造体内のフラグで、現在のMがGCを実行中であるか、またはGCをヘルプしている状態であるかを示します。
  4. システムコール (Syscall):

    • ユーザープログラムがOSの機能(ファイルI/O、ネットワーク通信、メモリ割り当てなど)を利用するために、OSのカーネルに要求を出すためのインターフェースです。GoプログラムがOSの機能を利用する際には、システムコールが発行されます。
    • Windowsにおけるシステムコール: Windowsでは、多くのシステムコールがDLL(ダイナミックリンクライブラリ)を介して行われ、GoランタイムからはCgo呼び出しと同様の「外部ライブラリ呼び出し」(libcall)として扱われることがあります。
  5. Cgo:

    • GoプログラムからC言語のコードを呼び出すためのGoの機能です。Cgoを介して呼び出されたCコードは、Goランタイムの管理外で実行されるため、Goプロファイラがそのスタックを直接アンワインドすることは困難です。
    • mp->curg->syscallpc / mp->curg->syscallsp: 現在のGoルーチン(curg)がシステムコールまたはCgo呼び出しを行っている場合、その呼び出しが行われたGo側のプログラムカウンタとスタックポインタを保持するフィールドです。
  6. etext:

    • Goバイナリの「テキストセグメント」(実行可能コードが格納されている領域)の終わりを示すシンボルです。etextより大きいアドレスは、通常、Goのコードではない外部のコード(例: Cライブラリ、OSカーネル)を示唆します。
  7. PCQuantum:

    • Goのプロファイリングにおいて、プログラムカウンタ(PC)の値を調整するために使用される定数です。これは、特定の命令の直前ではなく、その命令が属する関数の開始点に近い場所を指すように調整することで、より正確な関数名をプロファイルに表示するために使われます。

技術的詳細

このコミットの技術的な核心は、CPUプロファイリングのサンプリング時に、GoランタイムがGoのスタックをアンワインドできない状況(GC、システムコール、Cgo)に遭遇した場合の処理を改善することにあります。

  1. M構造体へのlibcall関連フィールドの追加:

    • src/pkg/runtime/runtime.hにおいて、M構造体に以下の3つのフィールドが追加されました。
      • uintptr libcallpc;: 外部ライブラリ呼び出し(特にWindowsのシステムコール)が行われたGo側のプログラムカウンタ。
      • uintptr libcallsp;: 外部ライブラリ呼び出しが行われたGo側のスタックポインタ。
      • G* libcallg;: 外部ライブラリ呼び出しを行ったGoルーチンへのポインタ。
    • これらのフィールドは、Goコードが外部ライブラリ(DLLなど)を呼び出す直前に、その呼び出し元のGoスタックのコンテキストを保存するために使用されます。
  2. Windowsにおけるlibcall情報の保存:

    • src/pkg/runtime/os_windows.cruntime·stdcall関数(Windowsの標準呼び出し規約で外部関数を呼び出すためのラッパー)と、src/pkg/runtime/sys_windows_386.sおよびsrc/pkg/runtime/sys_windows_amd64.sのアセンブリコード(runtime·usleep1など、Windows固有のシステムコールラッパー)が変更されました。
    • これらの箇所で、外部関数呼び出しを行う直前に、現在のGoルーチンのPC、SP、およびGをm->libcallpcm->libcallspm->libcallgに保存するようになりました。
    • 呼び出しが完了した後には、m->libcallspが0にリセットされ、古い情報が残らないようにしています。
  3. runtime·sigprofにおけるスタックトレースの改善:

    • src/pkg/runtime/proc.cruntime·sigprof関数(CPUプロファイリングのシグナルハンドラ)が大幅に修正されました。
    • 従来の挙動: 以前は、通常のruntime·gentracebackでスタックトレースが取得できない場合、単に現在のPCとSystem + 1という汎用的なフレームを報告していました。
    • 新しい挙動:
      • まず、通常のruntime·gentracebackを試みます。
      • それが失敗した場合、以下の特殊なケースを試みます。
        • Cgo呼び出しの検出: mp->ncgo > 0(Cgo呼び出し中であること)かつmp->curg->syscallpc != 0およびmp->curg->syscallsp != 0(Cgo呼び出し元のGoスタック情報があること)の場合、mp->curg->syscallpcmp->curg->syscallspを使ってGoスタックを再構築します。これにより、Cgo呼び出しに費やされたCPU時間が、そのCgoを呼び出したGoコードに帰属されるようになります。
        • Windowsのlibcall検出: GOOS_windowsの場合、mp->libcallg != nilかつmp->libcallpc != 0およびmp->libcallsp != 0の場合、mp->libcallpcmp->libcallspを使ってGoスタックを再構築します。これは、Windowsのシステムコールがlibcallとして扱われる場合に特に重要です。
      • 上記のいずれの試みも失敗した場合、最終的なフォールバックとして、抽象的な「System」または「GC」フレームを報告します。
        • 現在のPCがetext(Goコードの終わり)よりも大きい場合、ExternalCode + PCQuantumを報告します。これは、Goコード外の実行を示唆します。
        • mp->gcingまたはmp->helpgcフラグがセットされている場合(GC中またはGCをヘルプしている場合)、GC + PCQuantumを報告します。これにより、GC活動がプロファイルに明確に表示されるようになります。
        • それ以外の場合は、System + PCQuantumを報告します。
    • System, ExternalCode, GCという空の関数が追加されました。これらは、プロファイル上で特定のカテゴリを示すためのシンボルとしてのみ使用され、実際のコードは持ちません。PCQuantumが加算されるのは、プロファイラが関数シンボルを正確に解決できるようにするためです。

これらの変更により、CPUプロファイルは、Goランタイムの内部処理や外部呼び出しに起因するCPU使用率について、より詳細で意味のある情報を提供するようになります。

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

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

  1. src/pkg/runtime/os_windows.c:

    • runtime·stdcall関数内で、外部ライブラリ呼び出し(runtime·asmcgocall)の直前に、現在のGoルーチンのPC、SP、およびGをm->libcallpc, m->libcallsp, m->libcallgに保存するロジックが追加されました。
    • 呼び出し後にm->libcallspを0にリセットする行が追加されました。
  2. src/pkg/runtime/proc.c:

    • System, ExternalCode, GCという空の関数が定義されました。これらはプロファイリングのスタックフレームとして使用されます。
    • runtime·sigprof関数が大幅に修正されました。
      • 通常のスタックトレース取得が失敗した場合のフォールバックロジックが拡張されました。
      • Cgo呼び出し中(mp->ncgo > 0)の場合、mp->curg->syscallpcmp->curg->syscallspを使用してGoスタックを再構築する試みが追加されました。
      • Windows環境でlibcall中(mp->libcallg != nilなど)の場合、mp->libcallpcmp->libcallspを使用してGoスタックを再構築する試みが追加されました。
      • 最終的なフォールバックとして、GC中(mp->gcing || mp->helpgc)であればGCフレームを、Goコード外の実行であればExternalCodeフレームを、それ以外であればSystemフレームを報告するようになりました。
  3. src/pkg/runtime/runtime.h:

    • M構造体(struct M)に、CPUプロファイラで使用するためのlibcallpc, libcallsp, libcallgの3つのフィールドが追加されました。
  4. src/pkg/runtime/sys_windows_386.s および src/pkg/runtime/sys_windows_amd64.s:

    • runtime·usleep1などのアセンブリ関数内で、外部関数呼び出し(CALL AX)の直前に、現在のPC、SP、およびGをm_libcallpc(BP), m_libcallsp(BP), m_libcallg(BP)に保存するアセンブリ命令が追加されました。
    • 呼び出し後にm_libcallsp(BP)を0にリセットする命令が追加されました。

コアとなるコードの解説

src/pkg/runtime/os_windows.c の変更点

 // runtime·stdcall(void *fn, int32 count, ...)
 // ...
 	m->libcall.fn = fn;
 	m->libcall.n = count;
 	m->libcall.args = (uintptr*)&count + 1;
+	if(m->profilehz != 0) { // プロファイリングが有効な場合のみ
+		// leave pc/sp for cpu profiler
+		m->libcallpc = (uintptr)runtime·getcallerpc(&fn); // 呼び出し元のPCを取得
+		m->libcallsp = (uintptr)runtime·getcallersp(&fn); // 呼び出し元のSPを取得
+		m->libcallg = g; // 現在のGoルーチンを保存
+	}
 	runtime·asmcgocall(runtime·asmstdcall, &m->libcall); // 実際の外部呼び出し
+	m->libcallsp = 0; // 呼び出し完了後、SP情報をクリア
 	return (void*)m->libcall.r1;
 }

このコードは、Windowsで外部DLL関数を呼び出す際のGoランタイムのラッパーです。CPUプロファイリングが有効な場合(m->profilehz != 0)、外部呼び出しを行う直前に、その呼び出し元のGoコードのプログラムカウンタ(PC)、スタックポインタ(SP)、および現在のGoルーチン(G)の情報をM構造体のlibcallpc, libcallsp, libcallgフィールドに保存します。これにより、プロファイラが外部呼び出し中にサンプリングされた場合でも、その呼び出しをトリガーしたGoコードのコンテキストを特定できるようになります。呼び出しが完了したら、libcallspをクリアして、古い情報がプロファイラに誤って使用されないようにします。

src/pkg/runtime/proc.c の変更点

 // ...
 static void System(void) {}
 static void ExternalCode(void) {}
 static void GC(void) {}
 extern byte etext[]; // Goコードセグメントの終わりを示すシンボル
 // ...

 // runtime·sigprof (CPUプロファイリングシグナルハンドラ)
 // ...
 	if(!traceback || n <= 0) { // 通常のトレースバックが不可能または失敗した場合
 		// Normal traceback is impossible or has failed.
 		// See if it falls into several common cases.
 		n = 0;
 		if(mp->ncgo > 0 && mp->curg != nil &&
 		   mp->curg->syscallpc != 0 && mp->curg->syscallsp != 0) {
 			// Cgo呼び出し中であれば、Cgo呼び出し元のGoスタックを再構築
 			// This is especially important on windows, since all syscalls are cgo calls.
 			n = runtime·gentraceback(mp->curg->syscallpc, mp->curg->syscallsp, 0, mp->curg, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);
 		}
 #ifdef GOOS_windows
 		if(n == 0 && mp->libcallg != nil && mp->libcallpc != 0 && mp->libcallsp != 0) {
 			// Windowsのlibcall中であれば、libcall呼び出し元のGoスタックを再構築
 			n = runtime·gentraceback(mp->libcallpc, mp->libcallsp, 0, mp->libcallg, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);
 		}
 #endif
 		if(n == 0) { // 上記のいずれも失敗した場合
 			// If all of the above has failed, account it against abstract "System" or "GC".
 			n = 2;
 			// "ExternalCode" is better than "etext".
 			if((uintptr)pc > (uintptr)etext) // Goコード外のPCであれば
 				pc = (byte*)ExternalCode + PCQuantum; // ExternalCodeフレームを割り当てる
 			prof.pcbuf[0] = (uintptr)pc;
 			if(mp->gcing || mp->helpgc) // GC中またはGCをヘルプ中であれば
 				prof.pcbuf[1] = (uintptr)GC + PCQuantum; // GCフレームを割り当てる
 			else
 				prof.pcbuf[1] = (uintptr)System + PCQuantum; // それ以外はSystemフレームを割り当てる
 		}
 	}
 // ...

このruntime·sigprof関数は、CPUプロファイリングのサンプリング時に呼び出されます。通常のGoスタックトレースが取得できない場合(!traceback || n <= 0)、このコミットで追加されたロジックが実行されます。

  1. Cgoスタックの取得: まず、Cgo呼び出し中であるかを確認し、もしそうであれば、Cgo呼び出し元のGoスタック情報(mp->curg->syscallpc, mp->curg->syscallsp)を使ってruntime·gentracebackを試みます。
  2. Windows libcallスタックの取得: Windowsの場合、さらにlibcall情報(mp->libcallpc, mp->libcallsp)が利用可能であれば、それを使ってGoスタックを再構築します。
  3. フォールバック: 上記のいずれの試みも失敗した場合、最終的なフォールバックとして、抽象的なフレームを割り当てます。
    • サンプリングされたPCがGoコードの範囲外(etextより大きい)であれば、ExternalCodeというシンボルを割り当てます。
    • 現在のMがGCを実行中またはGCをヘルプ中であれば、GCというシンボルを割り当てます。
    • それ以外の場合は、Systemというシンボルを割り当てます。 これらのシンボルは、プロファイル上でそれぞれの活動を明確に区別するために使用されます。PCQuantumが加算されるのは、プロファイラがシンボルを正確に解決できるようにするためです。

src/pkg/runtime/runtime.h の変更点

 // ...
 struct	M
 {
 	// ...
 	LibCall	libcall;
 	uintptr	libcallpc;	// for cpu profiler
 	uintptr	libcallsp;
 	G*	libcallg;
 // ...

M構造体にlibcallpc, libcallsp, libcallgが追加されたことを示しています。これらは、外部ライブラリ呼び出し(特にWindowsのシステムコール)の際に、呼び出し元のGoスタックのコンテキストを保存するために使用されます。

src/pkg/runtime/sys_windows_amd64.s の変更点 (386版も同様)

 // ...
 TEXT runtime·usleep1(SB),NOSPLIT,$0
 // ...
 	MOVQ	m(R15), R13 // MポインタをR13にロード

 	// leave pc/sp for cpu profiler
 	MOVQ	(SP), R12 // 現在のSPの値をR12にロード (呼び出し元のリターンアドレス)
 	MOVQ	R12, m_libcallpc(R13) // R12 (PC) を m->libcallpc に保存
 	LEAQ	8(SP), R12 // SP+8 (呼び出し元のスタックフレームの開始) をR12にロード
 	MOVQ	R12, m_libcallsp(R13) // R12 (SP) を m->libcallsp に保存
 	MOVQ	g(R13), R12 // 現在のGポインタをR12にロード
 	MOVQ	R12, m_libcallg(R13) // R12 (G) を m->libcallg に保存

 	MOVQ	m_g0(R13), R14
 	CMPQ	g(R15), R14
 	JNE	usleep1_switch
 	// executing on m->g0 already
 	CALL	AX // 実際のシステムコール呼び出し
 	JMP	usleep1_ret

 usleep1_switch:
 	// Switch to m->g0 stack and back.
 	MOVQ	(g_sched+gobuf_sp)(R14), R14
 	MOVQ	SP, -8(R14)
 	LEAQ	-8(R14), SP
 	CALL	AX
 	MOVQ	0(SP), SP

 usleep1_ret:
 	MOVQ	$0, m_libcallsp(R13) // 呼び出し完了後、m->libcallsp をクリア
 	RET
 // ...

このアセンブリコードは、Windowsのusleep1(Goのtime.Sleepなどが内部で利用する可能性のあるシステムコールラッパー)のような関数が、実際のシステムコール(CALL AX)を実行する直前に、呼び出し元のGoスタックのPC、SP、およびGの情報をM構造体に保存する処理を示しています。これにより、システムコール中にプロファイラがサンプリングした場合でも、Go側の呼び出し元を正確に特定できるようになります。システムコールから戻った後には、m_libcallspがクリアされます。

関連リンク

参考にした情報源リンク

  • GoのCPUプロファイリングに関する公式ドキュメントやブログ記事 (一般的なGoプロファイリングの理解のため)
  • GoランタイムのM, G, Pモデルに関する資料 (ランタイムの動作理解のため)
  • GoのGCに関する資料 (GCの動作理解のため)
  • Cgoに関する公式ドキュメント (Cgoの動作理解のため)
  • Goのソースコード内のコメントや関連するコミット履歴 (詳細な挙動の確認のため)
  • etextシンボルに関する一般的な情報 (リンカや実行ファイルの構造理解のため)
  • PCQuantumに関するGoのプロファイリングツールの実装に関する情報 (プロファイリングデータの解釈のため)
  • Windowsのシステムコールに関する一般的な情報 (Windows固有のlibcallの背景理解のため)
  • https://github.com/golang/go/commit/5e72fae9b2c4fddc67a5d8ea0aecf3f73234d83e (GitHub上のコミットページ)
  • https://golang.org/cl/61270043 (Goのコードレビューシステム上のチェンジリスト)