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

[インデックス 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プロファイリングのロジックに反映させる点にあります。

  1. M構造体へのblockedフィールドの追加: src/pkg/runtime/runtime.hにおいて、M構造体にbool blocked;という新しいフィールドが追加されました。このフィールドは、MがNote上でブロックされているかどうかを示すフラグとして機能します。

  2. ブロック状態の追跡: src/pkg/runtime/lock_futex.csrc/pkg/runtime/lock_sema.cは、それぞれfutexとセマフォを用いたスレッドの同期処理を実装しています。これらのファイル内のruntime·notesleepおよびnotetsleep関数は、MがNote上でスリープ(ブロック)する際にm->blocked = true;を設定し、スリープから復帰する際にm->blocked = false;を設定するように変更されました。これにより、Goランタイムは各Mのブロック状態を正確に把握できるようになります。

  3. 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に限定されるようになりました。

  4. スケジューラトレースへの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·notesleepnotetsleep関数内で、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·notesleepnotetsleep関数内で、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->blockedtrueの場合にプロファイリングをスキップする条件が追加されました。

--- 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プロファイリングの対象を絞り込むようにした点です。

  1. M構造体へのblockedフィールドの追加: runtime.hM構造体にbool blocked;が追加されたことで、各OSスレッドが現在Note上でブロックされているかどうかをランタイムが内部的に管理できるようになりました。これは、スレッドがアイドル状態であるかどうかの判断基準となります。

  2. notesleep系関数でのblockedフラグの管理: lock_futex.clock_sema.c内のruntime·notesleepおよびnotetsleep関数は、GoランタイムがOSスレッドをブロック状態にする際に呼び出される低レベルの関数です。これらの関数内で、スレッドがブロック状態に入る直前にm->blocked = true;が設定され、ブロック状態から復帰する直前にm->blocked = false;が設定されます。これにより、m->blockedフラグは常にMの実際のブロック状態を反映するようになります。

  3. Windowsプロファイリングループでのblockedフラグの利用: os_windows.cruntime·profileloop1関数は、Windows上でCPUプロファイリングを行う際に、すべてのMを巡回してサンプリングを行います。この関数内で、各Mのblockedフラグがチェックされるようになりました。 if(thread == nil || mp->profilehz == 0 || mp->blocked)という条件は、以下のいずれかのMをプロファイリング対象から除外します。

    • OSスレッドがまだ存在しないか、既に終了しているM。
    • プロファイリングが無効になっているM(profilehz == 0)。
    • Note上でブロックされているM(mp->blockedtrue。 この変更により、アイドル状態のMはプロファイリングの対象から外れ、プロファイル結果の「etext」への偏りが解消されます。
  4. スケジューラトレースでのblocked状態の可視化: proc.cruntime·schedtrace関数は、Goランタイムの内部状態をデバッグログとして出力します。このログにm->blockedの値が追加されたことで、開発者はMがブロックされているかどうかを簡単に確認できるようになり、デバッグやランタイムの挙動分析に役立ちます。

これらの変更は、GoランタイムがOSスレッドのライフサイクルと状態をより細かく制御し、プロファイリングのような重要なツールがより正確な情報を提供するようにするための改善です。

関連リンク

参考にした情報源リンク

  • 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モデルに関する一般的な解説記事