[インデックス 18439] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境でのCPUプロファイリングの挙動を修正するものです。具体的には、アイドル状態のスレッドがCPUプロファイリングの対象から除外されるように変更され、これによりプロファイリング結果の精度が向上しました。
コミット
commit 0229dc6dbe969ee06f0e1f13df70b9c7fead68dd
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 10 15:40:55 2014 +0400
runtime: do not cpu profile idle threads on windows
Currently this leads to a significant skew towards 'etext' entry,
since all idle threads are profiled every tick.
Before:
Total: 66608 samples
63188 94.9% 94.9% 63188 94.9% etext
278 0.4% 95.3% 278 0.4% sweepspan
216 0.3% 95.6% 448 0.7% runtime.mallocgc
122 0.2% 95.8% 122 0.2% scanblock
113 0.2% 96.0% 113 0.2% net/textproto.canonicalMIMEHeaderKey
After:
Total: 8008 samples
3949 49.3% 49.3% 3949 49.3% etext
231 2.9% 52.2% 231 2.9% scanblock
211 2.6% 54.8% 211 2.6% runtime.cas64
182 2.3% 57.1% 408 5.1% runtime.mallocgc
178 2.2% 59.3% 178 2.2% runtime.atomicload64
LGTM=alex.brainman
R=golang-codereviews, alex.brainman
CC=golang-codereviews
https://golang.org/cl/61250043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0229dc6dbe969ee06f0e1f13df70b9c7fead68dd
元コミット内容
このコミットの目的は、Windows環境においてCPUプロファイリングがアイドル状態のスレッドを誤ってプロファイルしてしまう問題を解決することです。これにより、プロファイリング結果が「etext」エントリに大きく偏るという問題が発生していました。コミットメッセージに示されている「Before」と「After」のプロファイルデータは、この変更によって「etext」のサンプル数が大幅に減少し、より実態に即したプロファイル結果が得られるようになったことを明確に示しています。
変更の背景
Goランタイムは、プログラムの実行状況を分析するためにCPUプロファイリング機能を提供しています。これは、どの関数がCPU時間を多く消費しているかを特定し、パフォーマンスのボトルネックを解消するために不可欠なツールです。しかし、Windows環境では、Goランタイムが管理するOSスレッド(M: Machine)がアイドル状態(例えば、イベントの発生を待っている状態)であっても、CPUプロファイリングの対象となっていました。
具体的には、Goのプロファイラは定期的に各スレッドの状態をサンプリングします。アイドル状態のスレッドは、実際にはCPUを消費していないにもかかわらず、プロファイリングのサンプリング時に「etext」(実行可能コードセグメント、または未分類のCPU時間を示す一般的なカテゴリ)としてカウントされていました。これにより、プロファイル結果の大部分が「etext」に占められ、実際にCPUを消費している有意義な処理のプロファイルデータが埋もれてしまい、パフォーマンス分析の妨げとなっていました。
このコミットは、このような誤ったプロファイリングを排除し、真にCPUを消費しているスレッドのみを対象とすることで、プロファイル結果の正確性と有用性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とプロファイリングの仕組みに関する知識が必要です。
- Goroutine (G): Go言語における軽量な実行単位です。数百万のGoroutineを同時に実行できます。
- Logical Processor (P): Goroutineを実行するためのコンテキストを提供します。PはOSスレッド(M)にアタッチされ、Goroutineのスケジューリングを担当します。
- Machine (M): OSスレッドを表します。GoランタイムはMを管理し、Pを介してGoroutineを実行します。Mは、Goroutineの実行、システムコール、ネットワークI/Oなど、様々なタスクを実行します。Mは、Goroutineがブロックされたり、アイドル状態になったりすると、OSによって一時的に停止されることがあります。
- CPUプロファイリング: プログラムがCPU時間をどのように使用しているかを分析する手法です。GoのCPUプロファイラは、定期的に(例えば100Hzで)実行中のGoroutineのスタックトレースをサンプリングし、どの関数がCPUを消費しているかを統計的に推定します。
Note
: Goランタイム内部で使用される同期プリミティブの一つで、スレッドが特定のイベントを待機するために使用されます。例えば、Goroutineがチャネル操作やミューテックスのロック解除を待つ際に、関連するMがNote
上でブロックされることがあります。futex
(Fast Userspace Mutex): LinuxなどのUnix系OSで利用される、ユーザー空間での高速なミューテックス実装を可能にするシステムコールです。Goランタイムは、スレッドの同期のためにこれを利用します。semaphore
(セマフォ): スレッド間の同期メカニズムの一つで、リソースへのアクセスを制御するために使用されます。Goランタイムは、スレッドの待機や通知のためにセマフォを利用します。SuspendThread
/ResumeThread
(Windows API): Windows OSでスレッドの実行を一時停止したり再開したりするためのAPIです。Goのプロファイラは、プロファイリング対象のスレッドのコンテキストを安全に取得するためにこれらのAPIを使用します。
技術的詳細
このコミットの技術的な核心は、GoランタイムのM(OSスレッド)がブロック状態にあるかどうかを正確に追跡し、その情報をCPUプロファイリングのロジックに反映させる点にあります。
-
M
構造体へのblocked
フィールドの追加:src/pkg/runtime/runtime.h
において、M
構造体にbool blocked;
という新しいフィールドが追加されました。このフィールドは、MがNote
上でブロックされているかどうかを示すフラグとして機能します。 -
ブロック状態の追跡:
src/pkg/runtime/lock_futex.c
とsrc/pkg/runtime/lock_sema.c
は、それぞれfutex
とセマフォを用いたスレッドの同期処理を実装しています。これらのファイル内のruntime·notesleep
およびnotetsleep
関数は、MがNote
上でスリープ(ブロック)する際にm->blocked = true;
を設定し、スリープから復帰する際にm->blocked = false;
を設定するように変更されました。これにより、Goランタイムは各Mのブロック状態を正確に把握できるようになります。 -
Windows CPUプロファイリングの修正:
src/pkg/runtime/os_windows.c
内のruntime·profileloop1
関数は、Windows環境でのCPUプロファイリングのメインループです。この関数は、Goランタイムが管理するすべてのMをイテレートし、それぞれのスレッドを一時停止してプロファイル情報を収集します。 変更前は、thread == nil
の場合にのみプロファイリングをスキップしていましたが、変更後はthread == nil || mp->profilehz == 0 || mp->blocked
という条件が追加されました。thread == nil
: スレッドがまだOSにアタッチされていないか、既に終了している場合。mp->profilehz == 0
: そのMがプロファイリング対象外に設定されている場合。mp->blocked
: このコミットで追加された重要な条件。MがNote
上でブロックされている(アイドル状態である)場合、そのスレッドはCPUプロファイリングの対象から除外されます。
さらに、
profilem(mp)
の呼び出し前にもmp->profilehz != 0 && !mp->blocked
という条件が追加され、プロファイリングの実行がblocked
状態でないMに限定されるようになりました。 -
スケジューラトレースへの
blocked
状態の表示:src/pkg/runtime/proc.c
内のruntime·schedtrace
関数は、Goスケジューラの詳細な状態をデバッグ目的で出力します。この関数にm->blocked
の状態が出力されるように変更され、デバッグや分析の際にMのブロック状態を確認できるようになりました。
これらの変更により、Windows環境でのCPUプロファイリングは、実際にCPUを消費しているアクティブなスレッドのみを対象とするようになり、アイドル状態のスレッドによるプロファイル結果の歪みが解消されました。
コアとなるコードの変更箇所
src/pkg/runtime/runtime.h
M
構造体にblocked
フィールドが追加されました。
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -312,7 +312,8 @@ struct M
int32 dying;
int32 profilehz;
int32 helpgc;
- bool spinning;
+ bool spinning; // M is out of work and is actively looking for work
+ bool blocked; // M is blocked on a Note
uint32 fastrand;
uint64 ncgocall; // number of cgo calls in total
int32 ncgo; // number of cgo calls currently in progress
src/pkg/runtime/lock_futex.c
runtime·notesleep
とnotetsleep
関数内で、m->blocked
フラグが設定・解除されるようになりました。
--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -130,8 +130,11 @@ runtime·notesleep(Note *n)
{
if(g != m->g0)
runtime·throw("notesleep not on g0");
- while(runtime·atomicload((uint32*)&n->key) == 0)
+ while(runtime·atomicload((uint32*)&n->key) == 0) {
+ m->blocked = true;
runtime·futexsleep((uint32*)&n->key, 0, -1);
+ m->blocked = false;
+ }
}
#pragma textflag NOSPLIT
@@ -143,8 +146,11 @@ notetsleep(Note *n, int64 ns, int64 deadline, int64 now)
// does not count against our nosplit stack sequence.
if(ns < 0) {
- while(runtime·atomicload((uint32*)&n->key) == 0)
+ while(runtime·atomicload((uint32*)&n->key) == 0) {
+ m->blocked = true;
runtime·futexsleep((uint32*)&n->key, 0, -1);
+ m->blocked = false;
+ }
return true;
}
@@ -153,7 +159,9 @@ notetsleep(Note *n, int64 ns, int64 deadline, int64 now)
deadline = runtime·nanotime() + ns;
for(;;) {
+ m->blocked = true;
runtime·futexsleep((uint32*)&n->key, 0, ns);
+ m->blocked = false;
if(runtime·atomicload((uint32*)&n->key) != 0)
break;
now = runtime·nanotime();
src/pkg/runtime/lock_sema.c
runtime·notesleep
とnotetsleep
関数内で、m->blocked
フラグが設定・解除されるようになりました。
--- a/src/pkg/runtime/lock_sema.c
+++ b/src/pkg/runtime/lock_sema.c
@@ -161,7 +161,9 @@ runtime·notesleep(Note *n)
return;
}
// Queued. Sleep.
+ m->blocked = true;
runtime·semasleep(-1);
+ m->blocked = false;
}
#pragma textflag NOSPLIT
@@ -181,18 +183,23 @@ notetsleep(Note *n, int64 ns, int64 deadline, M *mp)
if(ns < 0) {
// Queued. Sleep.
+ m->blocked = true;
runtime·semasleep(-1);
+ m->blocked = false;
return true;
}
deadline = runtime·nanotime() + ns;
for(;;) {
// Registered. Sleep.
+ m->blocked = true;
if(runtime·semasleep(ns) >= 0) {
+ m->blocked = false;
// Acquired semaphore, semawakeup unregistered us.
// Done.
return true;
}
+ m->blocked = false;
// Interrupted or timed out. Still registered. Semaphore not acquired.\
ns = deadline - runtime·nanotime();
@@ -214,8 +221,10 @@ notetsleep(Note *n, int64 ns, int64 deadline, M *mp)
} else if(mp == (M*)LOCKED) {
// Wakeup happened so semaphore is available.
// Grab it to avoid getting out of sync.
+ m->blocked = true;
if(runtime·semasleep(-1) < 0)
runtime·throw("runtime: unable to acquire - semaphore out of sync");
+ m->blocked = false;
return true;
} else
runtime·throw("runtime: unexpected waitm - semaphore out of sync");
src/pkg/runtime/os_windows.c
runtime·profileloop1
関数内で、mp->blocked
がtrue
の場合にプロファイリングをスキップする条件が追加されました。
--- a/src/pkg/runtime/os_windows.c
+++ b/src/pkg/runtime/os_windows.c
@@ -426,10 +426,13 @@ runtime·profileloop1(void)\
allm = runtime·atomicloadp(&runtime·allm);
for(mp = allm; mp != nil; mp = mp->alllink) {
thread = runtime·atomicloadp(&mp->thread);
- if(thread == nil)
+ // Do not profile threads blocked on Notes,
+ // this includes idle worker threads,
+ // idle timer thread, idle heap scavenger, etc.
+ if(thread == nil || mp->profilehz == 0 || mp->blocked)
continue;
runtime·stdcall(runtime·SuspendThread, 1, thread);
- if(mp->profilehz != 0)
+ if(mp->profilehz != 0 && !mp->blocked)
profilem(mp);
runtime·stdcall(runtime·ResumeThread, 1, thread);
}
src/pkg/runtime/proc.c
runtime·schedtrace
関数内で、Mのblocked
状態が出力されるようになりました。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2690,10 +2690,10 @@ runtime·schedtrace(bool detailed)
if(lockedg)
id3 = lockedg->goid;
runtime·printf(" M%d: p=%D curg=%D mallocing=%d throwing=%d gcing=%d"\
- " locks=%d dying=%d helpgc=%d spinning=%d lockedg=%D\n",
+ " locks=%d dying=%d helpgc=%d spinning=%d blocked=%d lockedg=%D\n",
mp->id, id1, id2,
mp->mallocing, mp->throwing, mp->gcing, mp->locks, mp->dying, mp->helpgc,\
- mp->spinning, id3);
+ mp->spinning, m->blocked, id3);
}
runtime·lock(&allglock);
for(gi = 0; gi < runtime·allglen; gi++) {
コアとなるコードの解説
このコミットの主要な変更は、GoランタイムがOSスレッド(M)のブロック状態を正確に追跡し、その情報に基づいてCPUプロファイリングの対象を絞り込むようにした点です。
-
M
構造体へのblocked
フィールドの追加:runtime.h
でM
構造体にbool blocked;
が追加されたことで、各OSスレッドが現在Note
上でブロックされているかどうかをランタイムが内部的に管理できるようになりました。これは、スレッドがアイドル状態であるかどうかの判断基準となります。 -
notesleep
系関数でのblocked
フラグの管理:lock_futex.c
とlock_sema.c
内のruntime·notesleep
およびnotetsleep
関数は、GoランタイムがOSスレッドをブロック状態にする際に呼び出される低レベルの関数です。これらの関数内で、スレッドがブロック状態に入る直前にm->blocked = true;
が設定され、ブロック状態から復帰する直前にm->blocked = false;
が設定されます。これにより、m->blocked
フラグは常にMの実際のブロック状態を反映するようになります。 -
Windowsプロファイリングループでの
blocked
フラグの利用:os_windows.c
のruntime·profileloop1
関数は、Windows上でCPUプロファイリングを行う際に、すべてのMを巡回してサンプリングを行います。この関数内で、各Mのblocked
フラグがチェックされるようになりました。if(thread == nil || mp->profilehz == 0 || mp->blocked)
という条件は、以下のいずれかのMをプロファイリング対象から除外します。- OSスレッドがまだ存在しないか、既に終了しているM。
- プロファイリングが無効になっているM(
profilehz == 0
)。 Note
上でブロックされているM(mp->blocked
がtrue
)。 この変更により、アイドル状態のMはプロファイリングの対象から外れ、プロファイル結果の「etext」への偏りが解消されます。
-
スケジューラトレースでの
blocked
状態の可視化:proc.c
のruntime·schedtrace
関数は、Goランタイムの内部状態をデバッグログとして出力します。このログにm->blocked
の値が追加されたことで、開発者はMがブロックされているかどうかを簡単に確認できるようになり、デバッグやランタイムの挙動分析に役立ちます。
これらの変更は、GoランタイムがOSスレッドのライフサイクルと状態をより細かく制御し、プロファイリングのような重要なツールがより正確な情報を提供するようにするための改善です。
関連リンク
- Go CPU Profiling: https://go.dev/blog/pprof
- Go Runtime Scheduler: https://go.dev/doc/articles/go_scheduler.html
- Go CL 61250043 (Gerrit Code Review): https://golang.org/cl/61250043
参考にした情報源リンク
- Goソースコード (特に
src/pkg/runtime
ディレクトリ) - Go公式ブログ
- Goドキュメント
- Gerrit Code Review System
- Windows API ドキュメント (SuspendThread, ResumeThread)
- futex(2) - Linux man page
- semaphore(7) - Linux man page
- GoのM, P, Gモデルに関する一般的な解説記事