[インデックス 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」という一般的なカテゴリにまとめられており、これは具体的なパフォーマンスボトルネックを特定する上で十分な情報を提供していませんでした。
この変更により、以下の改善が図られます。
- GCフレームの追加: GC活動中に消費されたCPU時間に対して、明確な「GC」親フレームがプロファイルスタックに追加されます。これにより、GCがどの程度のCPU時間を消費しているかをより正確に把握できるようになります。
- システムコール/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開発者がパフォーマンスの問題をより効率的に診断・解決できるようにすることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下の概念について基本的な知識が必要です。
-
CPUプロファイリング:
- サンプリングプロファイラ: GoのCPUプロファイラはサンプリングベースです。これは、一定の間隔(例: 100Hz、つまり1秒間に100回)でプログラムの実行を一時停止し、その時点でのプログラムカウンタ(PC)と呼び出しスタック(コールスタック)を記録します。
- コールスタック(呼び出しスタック): プログラムが実行されているときに、現在実行中の関数から、その関数を呼び出した関数、さらにその関数を呼び出した関数へと遡る一連の関数のリストです。プロファイラはこれを使って、どの関数がCPU時間を消費しているかを特定します。
- スタックアンワインディング: コールスタックを遡って、各関数の呼び出し元を特定するプロセスです。これは、プログラムカウンタとスタックポインタ(SP)の情報を使って行われます。
-
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の呼び出しスタックを再構築します。
-
ガベージコレクション (GC):
- Goの自動メモリ管理機能です。不要になったメモリを自動的に解放し、メモリリークを防ぎます。GCはプログラムの実行中にバックグラウンドで動作し、CPU時間を消費します。
mp->gcing
/mp->helpgc
:M
構造体内のフラグで、現在のMがGCを実行中であるか、またはGCをヘルプしている状態であるかを示します。
-
システムコール (Syscall):
- ユーザープログラムがOSの機能(ファイルI/O、ネットワーク通信、メモリ割り当てなど)を利用するために、OSのカーネルに要求を出すためのインターフェースです。GoプログラムがOSの機能を利用する際には、システムコールが発行されます。
- Windowsにおけるシステムコール: Windowsでは、多くのシステムコールがDLL(ダイナミックリンクライブラリ)を介して行われ、GoランタイムからはCgo呼び出しと同様の「外部ライブラリ呼び出し」(libcall)として扱われることがあります。
-
Cgo:
- GoプログラムからC言語のコードを呼び出すためのGoの機能です。Cgoを介して呼び出されたCコードは、Goランタイムの管理外で実行されるため、Goプロファイラがそのスタックを直接アンワインドすることは困難です。
mp->curg->syscallpc
/mp->curg->syscallsp
: 現在のGoルーチン(curg
)がシステムコールまたはCgo呼び出しを行っている場合、その呼び出しが行われたGo側のプログラムカウンタとスタックポインタを保持するフィールドです。
-
etext
:- Goバイナリの「テキストセグメント」(実行可能コードが格納されている領域)の終わりを示すシンボルです。
etext
より大きいアドレスは、通常、Goのコードではない外部のコード(例: Cライブラリ、OSカーネル)を示唆します。
- Goバイナリの「テキストセグメント」(実行可能コードが格納されている領域)の終わりを示すシンボルです。
-
PCQuantum
:- Goのプロファイリングにおいて、プログラムカウンタ(PC)の値を調整するために使用される定数です。これは、特定の命令の直前ではなく、その命令が属する関数の開始点に近い場所を指すように調整することで、より正確な関数名をプロファイルに表示するために使われます。
技術的詳細
このコミットの技術的な核心は、CPUプロファイリングのサンプリング時に、GoランタイムがGoのスタックをアンワインドできない状況(GC、システムコール、Cgo)に遭遇した場合の処理を改善することにあります。
-
M
構造体へのlibcall
関連フィールドの追加:src/pkg/runtime/runtime.h
において、M
構造体に以下の3つのフィールドが追加されました。uintptr libcallpc;
: 外部ライブラリ呼び出し(特にWindowsのシステムコール)が行われたGo側のプログラムカウンタ。uintptr libcallsp;
: 外部ライブラリ呼び出しが行われたGo側のスタックポインタ。G* libcallg;
: 外部ライブラリ呼び出しを行ったGoルーチンへのポインタ。
- これらのフィールドは、Goコードが外部ライブラリ(DLLなど)を呼び出す直前に、その呼び出し元のGoスタックのコンテキストを保存するために使用されます。
-
Windowsにおける
libcall
情報の保存:src/pkg/runtime/os_windows.c
のruntime·stdcall
関数(Windowsの標準呼び出し規約で外部関数を呼び出すためのラッパー)と、src/pkg/runtime/sys_windows_386.s
およびsrc/pkg/runtime/sys_windows_amd64.s
のアセンブリコード(runtime·usleep1
など、Windows固有のシステムコールラッパー)が変更されました。- これらの箇所で、外部関数呼び出しを行う直前に、現在のGoルーチンのPC、SP、およびGを
m->libcallpc
、m->libcallsp
、m->libcallg
に保存するようになりました。 - 呼び出しが完了した後には、
m->libcallsp
が0にリセットされ、古い情報が残らないようにしています。
-
runtime·sigprof
におけるスタックトレースの改善:src/pkg/runtime/proc.c
のruntime·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->syscallpc
とmp->curg->syscallsp
を使ってGoスタックを再構築します。これにより、Cgo呼び出しに費やされたCPU時間が、そのCgoを呼び出したGoコードに帰属されるようになります。 - Windowsの
libcall
検出:GOOS_windows
の場合、mp->libcallg != nil
かつmp->libcallpc != 0
およびmp->libcallsp != 0
の場合、mp->libcallpc
とmp->libcallsp
を使ってGoスタックを再構築します。これは、Windowsのシステムコールがlibcall
として扱われる場合に特に重要です。
- Cgo呼び出しの検出:
- 上記のいずれの試みも失敗した場合、最終的なフォールバックとして、抽象的な「System」または「GC」フレームを報告します。
- 現在のPCが
etext
(Goコードの終わり)よりも大きい場合、ExternalCode + PCQuantum
を報告します。これは、Goコード外の実行を示唆します。 mp->gcing
またはmp->helpgc
フラグがセットされている場合(GC中またはGCをヘルプしている場合)、GC + PCQuantum
を報告します。これにより、GC活動がプロファイルに明確に表示されるようになります。- それ以外の場合は、
System + PCQuantum
を報告します。
- 現在のPCが
- まず、通常の
System
,ExternalCode
,GC
という空の関数が追加されました。これらは、プロファイル上で特定のカテゴリを示すためのシンボルとしてのみ使用され、実際のコードは持ちません。PCQuantum
が加算されるのは、プロファイラが関数シンボルを正確に解決できるようにするためです。
これらの変更により、CPUプロファイルは、Goランタイムの内部処理や外部呼び出しに起因するCPU使用率について、より詳細で意味のある情報を提供するようになります。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコード箇所は以下の通りです。
-
src/pkg/runtime/os_windows.c
:runtime·stdcall
関数内で、外部ライブラリ呼び出し(runtime·asmcgocall
)の直前に、現在のGoルーチンのPC、SP、およびGをm->libcallpc
,m->libcallsp
,m->libcallg
に保存するロジックが追加されました。- 呼び出し後に
m->libcallsp
を0にリセットする行が追加されました。
-
src/pkg/runtime/proc.c
:System
,ExternalCode
,GC
という空の関数が定義されました。これらはプロファイリングのスタックフレームとして使用されます。runtime·sigprof
関数が大幅に修正されました。- 通常のスタックトレース取得が失敗した場合のフォールバックロジックが拡張されました。
- Cgo呼び出し中(
mp->ncgo > 0
)の場合、mp->curg->syscallpc
とmp->curg->syscallsp
を使用してGoスタックを再構築する試みが追加されました。 - Windows環境で
libcall
中(mp->libcallg != nil
など)の場合、mp->libcallpc
とmp->libcallsp
を使用してGoスタックを再構築する試みが追加されました。 - 最終的なフォールバックとして、GC中(
mp->gcing || mp->helpgc
)であればGC
フレームを、Goコード外の実行であればExternalCode
フレームを、それ以外であればSystem
フレームを報告するようになりました。
-
src/pkg/runtime/runtime.h
:M
構造体(struct M
)に、CPUプロファイラで使用するためのlibcallpc
,libcallsp
,libcallg
の3つのフィールドが追加されました。
-
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
)、このコミットで追加されたロジックが実行されます。
- Cgoスタックの取得: まず、Cgo呼び出し中であるかを確認し、もしそうであれば、Cgo呼び出し元のGoスタック情報(
mp->curg->syscallpc
,mp->curg->syscallsp
)を使ってruntime·gentraceback
を試みます。 - Windows
libcall
スタックの取得: Windowsの場合、さらにlibcall
情報(mp->libcallpc
,mp->libcallsp
)が利用可能であれば、それを使ってGoスタックを再構築します。 - フォールバック: 上記のいずれの試みも失敗した場合、最終的なフォールバックとして、抽象的なフレームを割り当てます。
- サンプリングされたPCがGoコードの範囲外(
etext
より大きい)であれば、ExternalCode
というシンボルを割り当てます。 - 現在のMがGCを実行中またはGCをヘルプ中であれば、
GC
というシンボルを割り当てます。 - それ以外の場合は、
System
というシンボルを割り当てます。 これらのシンボルは、プロファイル上でそれぞれの活動を明確に区別するために使用されます。PCQuantum
が加算されるのは、プロファイラがシンボルを正確に解決できるようにするためです。
- サンプリングされたPCがGoコードの範囲外(
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 CL 61270043: https://golang.org/cl/61270043 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- 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のコードレビューシステム上のチェンジリスト)