[インデックス 17199] ファイルの概要
このコミットは、GoランタイムにおけるCPUプロファイリングの信号損失を防ぐための重要な変更を導入しています。具体的には、ガベージコレクション(GC)、スタックスプリット、スケジューラ、システムコールなど、これまでプロファイリングの対象外となっていた、あるいは正確に計測されていなかった処理からのプロファイリング信号を捕捉し、より正確なプロファイルデータを提供することを目指しています。
コミット
commit cc4e6aad8ec18b4ee7fe0392f30f229ddb979589
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Aug 13 22:12:02 2013 +0400
runtime: do no lose CPU profiling signals
Currently we lose lots of profiling signals.
Most notably, GC is not accounted at all.
But stack splits, scheduler, syscalls, etc are lost as well.
This creates seriously misleading profile.
With this change all profiling signals are accounted.
Now I see these additional entries that were previously absent:
161 29.7% 29.7% 164 30.3% syscall.Syscall
12 2.2% 50.9% 12 2.2% scanblock
11 2.0% 55.0% 11 2.0% markonly
10 1.8% 58.9% 10 1.8% sweepspan
2 0.4% 85.8% 2 0.4% runtime.newstack
It is still impossible to understand what causes stack splits,
but at least it's clear how many time is spent on them.
Update #2197.
Update #5659.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12179043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cc4e6aad8ec18b4ee7fe0392f30f229ddb979589
元コミット内容
runtime: do no lose CPU profiling signals
Currently we lose lots of profiling signals.
Most notably, GC is not accounted at all.
But stack splits, scheduler, syscalls, etc are lost as well.
This creates seriously misleading profile.
With this change all profiling signals are accounted.
Now I see these additional entries that were previously absent:
161 29.7% 29.7% 164 30.3% syscall.Syscall
12 2.2% 50.9% 12 2.2% scanblock
11 2.0% 55.0% 11 2.0% markonly
10 1.8% 58.9% 10 1.8% sweepspan
2 0.4% 85.8% 2 0.4% runtime.newstack
It is still impossible to understand what causes stack splits,
but at least it's clear how many time is spent on them.
Update #2197.
Update #5659.
変更の背景
GoのCPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを特定するための重要なツールです。しかし、このコミット以前は、プロファイリング信号(SIGPROF
)が特定の状況下で失われるという問題がありました。特に、ガベージコレクション(GC)の実行中、スタックスプリット(Goルーチンのスタックが拡張される処理)、スケジューラの動作、システムコールなど、ランタイム内部の重要な処理がプロファイルに適切に反映されていませんでした。
これにより、生成されるプロファイルデータは誤解を招く可能性があり、開発者はアプリケーションの真のパフォーマンスボトルネックを特定することが困難でした。例えば、GCが多くのCPU時間を消費しているにもかかわらず、プロファイルにはその情報が全く表示されない、といった状況が発生していました。
このコミットの目的は、これらの失われたプロファイリング信号を捕捉し、GoプログラムのCPU使用率をより正確に反映したプロファイルを提供することです。これにより、開発者はより信頼性の高いプロファイルデータに基づいて最適化を行うことができるようになります。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とプロファイリングの仕組みに関する知識が必要です。
-
CPUプロファイリングと
SIGPROF
: CPUプロファイリングは、一定の間隔(通常はタイマー割り込みによってトリガーされるSIGPROF
シグナル)でプログラムの実行状態(特にプログラムカウンタ)をサンプリングし、どの関数がCPU時間を消費しているかを統計的に推定する手法です。SIGPROF
はUnix系システムで利用されるシグナルで、プロファイリングのために定期的にプロセスに送信されます。 -
GoルーチンとM/P/Gモデル: Goランタイムは、Goルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つの主要なエンティティで構成されるスケジューラモデルを採用しています。
- G (Goroutine): Goの軽量な並行実行単位。
- P (Processor): Goルーチンを実行するための論理的なコンテキスト。OSスレッド(M)にアタッチされ、Goルーチンキューを管理します。
- M (Machine): OSスレッド。Goルーチンを実行したり、システムコールを実行したりします。
-
m->g0
とm->gsignal
: 各OSスレッド(M)には、特別なGoルーチンが2つ関連付けられています。m->g0
: ランタイムのスケジューラやGCなどの内部処理を実行するためのシステムスタックを持つGoルーチン。ユーザーコードは実行しません。m->gsignal
: シグナルハンドラが実行される際に使用される特別なGoルーチン。シグナルハンドラは通常のGoルーチンのスタックではなく、このgsignal
のスタックで実行されます。
-
runtime·gentraceback
: Goランタイム内部の関数で、指定されたプログラムカウンタ(PC)とスタックポインタ(SP)からスタックトレースを生成します。プロファイリングにおいては、SIGPROF
シグナルが捕捉された時点のPCとSPから、現在実行中の関数のコールスタックを取得するために使用されます。 -
スタックスプリット: Goルーチンのスタックは最初は小さく確保され、必要に応じて動的に拡張されます。この拡張処理をスタックスプリットと呼びます。スタックスプリットはランタイム内部の処理であり、ユーザーコードの実行とは異なるコンテキストで行われます。
-
レース検出器(Race Detector)と
asmcgocall
: Goのレース検出器は、並行処理におけるデータ競合を検出するためのツールです。asmcgocall
は、GoコードからCコード(またはアセンブリコード)を呼び出す際に使用されるランタイム内部のメカニズムです。レース検出器が有効な場合、特定のasmcgocall
の呼び出しは、通常のGoルーチンの実行コンテキストとは異なる状態になることがあります。
技術的詳細
このコミットの主要な変更は、src/pkg/runtime/proc.c
のruntime·sigprof
関数と、各アーキテクチャ固有のsignal_*.c
ファイル(signal_386.c
, signal_amd64.c
, signal_arm.c
)におけるruntime·sighandler
関数の変更にあります。
runtime·sigprof
関数の変更 (src/pkg/runtime/proc.c
)
以前のruntime·sigprof
関数では、プロファイリング信号が到着した際に、特定の条件(gp != m->g0 && gp != m->gsignal
)を満たさないGoルーチンからの信号は無視されていました。これは、m->g0
(ランタイム内部処理)やm->gsignal
(シグナルハンドラ)で発生したプロファイリング信号が、通常のGoルーチンのスタックトレースとは異なる性質を持つため、正確なスタックトレースの取得が困難であるという仮定に基づいていた可能性があります。しかし、これによりGCやスケジューラなどの重要なランタイム処理がプロファイルから完全に抜け落ちていました。
このコミットでは、runtime·sigprof
関数にtraceback
という新しいブール変数が導入されました。この変数は、スタックトレースを生成すべきかどうかを制御します。
変更後のロジックは以下のようになります。
- 初期化:
traceback
はデフォルトでtrue
に設定されます。 - Windowsの特殊処理: Windows環境ではプロファイリングが専用のスレッドで行われるため、
m
(OSスレッド)やmcache
(Mのキャッシュ)がnil
の場合でもtraceback
はtrue
のままです。それ以外のOSでは、m
またはmcache
がnil
の場合はtraceback
がfalse
に設定されます。これは、有効なMコンテキストがない場合にスタックトレースを試みても意味がないためです。 - 特殊なGoルーチンの除外:
gp == m->g0
(ランタイム内部処理用のGoルーチン)またはgp == m->gsignal
(シグナルハンドラ用のGoルーチン)の場合、traceback
はfalse
に設定されます。これは、これらのGoルーチンはユーザーコードとは異なる特殊なスタック構造を持つため、通常のruntime·gentraceback
では正確なスタックトレースが得られない可能性があるためです。 - レース検出器の考慮:
m != nil && m->racecall
の場合、traceback
はfalse
に設定されます。これは、レース検出器が有効な場合にasmcgocall
を介して行われる呼び出しは、スタックトレースが困難な特殊な状態になるためです。 - スタックトレースの試行:
traceback
がtrue
の場合にのみ、runtime·gentraceback
が呼び出され、スタックトレースが試みられます。 - フォールバックメカニズム:
traceback
がfalse
の場合、またはruntime·gentraceback
がスタックトレースを生成できなかった場合(n <= 0
)、プロファイルデータはPC(プログラムカウンタ)とSystem
関数のアドレス(+1)の2つのエントリで構成されます。prof.pcbuf[0] = (uintptr)pc;
: シグナルが発生した時点のプログラムカウンタ。prof.pcbuf[1] = (uintptr)System + 1;
: 新しく追加されたSystem
関数のアドレス。このSystem
関数は空の関数であり、そのアドレスをプロファイルに含めることで、スタックトレースが取得できないランタイム内部の処理(GC、スケジューラ、システムコールなど)を「System」というカテゴリで集計できるようにしています。+1
は、Goのスタックトレースにおいて、関数の開始アドレスではなく、その関数内の任意の命令のアドレスを示す慣習的なオフセットです。これにより、System
関数がプロファイル上で独立したエントリとして認識されやすくなります。
- プロファイルデータの記録: 最終的に、
prof.fn(prof.pcbuf, n)
が呼び出され、取得された(またはフォールバックされた)スタックトレースがプロファイラに記録されます。
runtime·sighandler
関数の変更 (src/pkg/runtime/signal_*.c
)
各アーキテクチャ固有のsignal_*.c
ファイルでは、SIGPROF
シグナルを処理するruntime·sighandler
関数から、以下の条件分岐が削除されました。
if(gp != m->g0 && gp != m->gsignal)
runtime·sigprof(...)
この変更により、SIGPROF
シグナルがm->g0
やm->gsignal
で発生した場合でも、無条件にruntime·sigprof
関数が呼び出されるようになりました。これにより、ランタイム内部の処理(GCなど)で発生したプロファイリング信号もruntime·sigprof
に渡され、前述のフォールバックメカニズムによって「System」として集計される道が開かれました。
コアとなるコードの変更箇所
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1990,26 +1990,45 @@ static struct {
uintptr pcbuf[100];
} prof;
+static void
+System(void)
+{
+}
+
// Called if we receive a SIGPROF signal.
void
runtime·sigprof(uint8 *pc, uint8 *sp, uint8 *lr, G *gp)
{
int32 n;
+ bool traceback;
-\t// Windows does profiling in a dedicated thread w/o m.\n-\tif(!Windows && (m == nil || m->mcache == nil))\n-\t\treturn;\n \tif(prof.fn == nil || prof.hz == 0)\n \t\treturn;\
+\ttraceback = true;
+\t// Windows does profiling in a dedicated thread w/o m.\n+\tif(!Windows && (m == nil || m->mcache == nil))\n+\t\ttraceback = false;
+\tif(gp == m->g0 || gp == m->gsignal)\
+\t\ttraceback = false;
+\t// Race detector calls asmcgocall w/o entersyscall/exitsyscall,\n+\t// we can not currently unwind through asmcgocall.\n+\tif(m != nil && m->racecall)\
+\t\ttraceback = false;
\truntime·lock(&prof);\
\tif(prof.fn == nil) {\
\t\truntime·unlock(&prof);\
\t\treturn;\
\t}\
-\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);\
-\tif(n > 0)\
-\t\tprof.fn(prof.pcbuf, n);\
+\tn = 0;
+\tif(traceback)\
+\t\tn = runtime·gentraceback((uintptr)pc, (uintptr)sp, (uintptr)lr, gp, 0, prof.pcbuf, nelem(prof.pcbuf), nil, nil, false);
+\tif(!traceback || n <= 0) {\
+\t\tn = 2;
+\t\tprof.pcbuf[0] = (uintptr)pc;
+\t\tprof.pcbuf[1] = (uintptr)System + 1;
+\t}
+\tprof.fn(prof.pcbuf, n);
\truntime·unlock(&prof);\
}
src/pkg/runtime/signal_386.c
, src/pkg/runtime/signal_amd64.c
, src/pkg/runtime/signal_arm.c
これらのファイルでは、runtime·sighandler
関数内のSIGPROF
シグナル処理部分から、gp != m->g0 && gp != m->gsignal
という条件が削除されています。
例: src/pkg/runtime/signal_386.c
--- a/src/pkg/runtime/signal_386.c
+++ b/src/pkg/runtime/signal_386.c
@@ -39,8 +39,7 @@ runtime·sighandler(int32 sig, Siginfo *info, void *ctxt, G *gp)
bool crash;
if(sig == SIGPROF) {
-\t\tif(gp != m->g0 && gp != m->gsignal)\
-\t\t\truntime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp);\
+\t\truntime·sigprof((byte*)SIG_EIP(info, ctxt), (byte*)SIG_ESP(info, ctxt), nil, gp);\
\treturn;\
}
コアとなるコードの解説
このコミットの核心は、runtime·sigprof
関数におけるプロファイリング信号の処理方法の変更と、runtime·sighandler
からの呼び出し条件の緩和です。
-
System
関数の導入:static void System(void) {}
という空の関数が追加されました。この関数のアドレスは、スタックトレースが取得できない場合にプロファイルデータに含められます。これにより、GCやスケジューラなどのランタイム内部処理が「System」という共通のカテゴリで集計され、プロファイル上で可視化されるようになります。これは、これらの処理の正確なコールスタックを追跡することが困難であるため、次善の策として導入されたものです。 -
traceback
変数の導入と条件付きスタックトレース:runtime·sigprof
関数内でbool traceback;
が導入され、スタックトレースを試みるべきかどうかを動的に判断するようになりました。gp == m->g0 || gp == m->gsignal
: ランタイム内部のGoルーチンやシグナルハンドラ用のGoルーチンでは、通常のスタックトレースが困難なため、traceback
がfalse
になります。m != nil && m->racecall
: レース検出器が有効な場合の特定のasmcgocall
のコンテキストでも、スタックトレースが困難なため、traceback
がfalse
になります。- これらの条件に合致する場合、
runtime·gentraceback
は呼び出されず、代わりにpc
とSystem
関数のアドレスがプロファイルデータとして記録されます。これにより、これらの特殊なコンテキストでのCPU時間もプロファイルに反映されるようになります。
-
runtime·sighandler
からの無条件呼び出し:signal_*.c
ファイルにおける変更は、SIGPROF
シグナルがm->g0
やm->gsignal
で発生した場合でも、runtime·sigprof
関数が呼び出されるようにした点です。これにより、GCなどのランタイム内部処理中に発生したプロファイリング信号がruntime·sigprof
に到達し、そこで新しいフォールバックメカニズムによって処理されるようになります。
これらの変更により、GoのCPUプロファイリングは、ユーザーコードだけでなく、ランタイム内部の重要な処理(GC、スタックスプリット、スケジューラ、システムコールなど)のCPU時間も正確に反映できるようになりました。これにより、プロファイルデータはより包括的で信頼性の高いものとなり、パフォーマンス分析の精度が向上します。
コミットメッセージに記載されているように、この変更によってsyscall.Syscall
、scanblock
、markonly
、sweepspan
、runtime.newstack
といったエントリがプロファイルに現れるようになったのは、これらの処理が以前は適切にプロファイルされていなかったことを示しています。特にGC関連の関数(scanblock
, markonly
, sweepspan
)がプロファイルに現れるようになったことは、GCのパフォーマンス特性を理解する上で非常に重要です。
関連リンク
- Go issue #2197: cmd/go: cpu profiling misses GC time
- Go issue #5659: runtime: cpu profiler misses time spent in runtime.newstack
- Go CL 12179043: https://golang.org/cl/12179043
参考にした情報源リンク
- GoのCPUプロファイリングに関する公式ドキュメントやブログ記事
- GoランタイムのM/P/Gモデルに関する解説
SIGPROF
シグナルとUnix系システムにおけるプロファイリングの仕組み- Goのガベージコレクションの仕組み
- Goのスタックスプリットに関する情報
- Goのレース検出器に関する情報
- Goのソースコード(特に
src/runtime
ディレクトリ) - Dmitriy Vyukov氏の他のGoランタイム関連コミットや議論