[インデックス 17017] ファイルの概要
このコミットは、GoランタイムにおけるWindows環境での動的優先度昇格(Dynamic Priority Boosting)を無効にする変更を導入しています。これにより、WindowsのスケジューリングメカニズムがGoの並行処理モデルと衝突し、パフォーマンス問題を引き起こす可能性を排除します。特に、I/O負荷の高いシナリオやユニプロセッサ環境でのタイマースレッドのスケジューリング遅延が改善されます。
コミット
commit a574822f8044204357236405dca60d1ca5123ab5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sun Aug 4 14:08:13 2013 +0400
runtime: disable dynamic priority boosting on windows
Windows dynamic priority boosting assumes that a process has different types
of dedicated threads -- GUI, IO, computational, etc. Go processes use
equivalent threads that all do a mix of GUI, IO, computations, etc.
In such context dynamic priority boosting does nothing but harm, so turn it off.
In particular, if 2 goroutines do heavy IO on a server uniprocessor machine,
windows rejects to schedule timer thread for 2+ seconds when priority boosting is enabled.
Fixes #5971.
R=alex.brainman
CC=golang-dev
https://golang.org/cl/12406043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a574822f8044204357236405dca60d1ca5123ab5
元コミット内容
runtime: disable dynamic priority boosting on windows
Windows dynamic priority boosting assumes that a process has different types
of dedicated threads -- GUI, IO, computational, etc. Go processes use
equivalent threads that all do a mix of GUI, IO, computations, etc.
In such context dynamic priority boosting does nothing but harm, so turn it off.
In particular, if 2 goroutines do heavy IO on a server uniprocessor machine,
windows rejects to schedule timer thread for 2+ seconds when priority boosting is enabled.
Fixes #5971.
変更の背景
この変更の背景には、Windowsオペレーティングシステムが持つスレッドスケジューリングの特性と、Go言語のランタイムがスレッドをどのように利用するかのミスマッチがあります。
Windowsのスケジューラは、通常、I/O完了やGUIイベントの発生など、特定のアクションを行ったスレッドの優先度を一時的に引き上げる「動的優先度昇格(Dynamic Priority Boosting)」というメカニズムを持っています。これは、ユーザーインタラクションやI/O処理の応答性を向上させることを目的としています。Windowsは、GUIスレッド、I/Oスレッド、計算スレッドなど、異なる種類の専用スレッドが存在することを前提としています。
しかし、Goランタイムは、これらのWindowsの前提とは異なる方法でスレッドを利用します。Goのランタイムは、少数のOSスレッド(M: Machine)上で多数のゴルーチン(G: Goroutine)を多重化して実行します。これらのOSスレッドは、GUI、I/O、計算など、あらゆる種類のタスクを混在して実行します。つまり、GoのOSスレッドは特定の役割に特化しているわけではなく、Goランタイムのスケジューラによって動的に様々なゴルーチンが割り当てられます。
このようなGoの設計思想とWindowsの動的優先度昇格が組み合わさると、予期せぬパフォーマンス問題が発生することが判明しました。特に、コミットメッセージに記載されているように、「2つのゴルーチンがサーバーのユニプロセッサマシンで大量のI/Oを行う場合、優先度昇格が有効になっていると、Windowsはタイマースレッドのスケジューリングを2秒以上拒否する」という問題が発生していました。これは、I/Oを行ったGoのOSスレッドの優先度が不必要に引き上げられ、他の重要なランタイムスレッド(例えば、タイマーを処理するスレッドや、他のゴルーチンをスケジューリングするスレッド)の実行が妨げられるためと考えられます。
この問題を解決し、GoアプリケーションがWindows上でより予測可能で効率的に動作するようにするために、動的優先度昇格を無効にする決定がなされました。これにより、GoランタイムがOSスレッドを均等に扱い、Goの内部スケジューリングがより効果的に機能するようになります。
前提知識の解説
1. Goランタイムとゴルーチン、OSスレッド
- ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムがゴルーチンのスケジューリングを管理します。
- OSスレッド (Operating System Thread): オペレーティングシステムが管理する実行単位です。CPUコア上で実際にコードを実行するのはOSスレッドです。
- Goランタイムのスケジューラ (M:Nスケジューリング): Goランタイムは、M個のゴルーチンをN個のOSスレッドに多重化して実行します(M:Nスケジューリングモデル)。これにより、OSスレッドの切り替えコストを最小限に抑えつつ、高い並行性を実現します。GoのOSスレッドは、特定のタスクに特化せず、I/O、計算、ネットワーク処理など、あらゆる種類のゴルーチンを実行します。
2. Windowsの動的優先度昇格 (Dynamic Priority Boosting)
Windowsのプロセスおよびスレッドスケジューラは、システムの応答性を向上させるために、スレッドの優先度を動的に調整するメカニズムを持っています。
- 優先度クラスと相対優先度: Windowsのスレッドは、プロセス全体の「優先度クラス」(例: NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS)と、そのクラス内での「相対優先度」(例: THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_ABOVE_NORMAL)の組み合わせで優先度が決定されます。
- 動的優先度昇格: 特定のイベント(例: I/O完了、GUIイベント、待機状態からの復帰)が発生すると、Windowsスケジューラは一時的にそのスレッドの動的優先度を引き上げます。これにより、そのスレッドがCPUをより早く獲得し、応答性が向上することが期待されます。この昇格は一時的なもので、スレッドがCPUを消費すると徐々に元の優先度に戻ります。
SetProcessPriorityBoost
関数: このWindows API関数は、プロセスのスレッドが動的優先度昇格を受けるかどうかを制御します。TRUE
(デフォルト)に設定すると昇格が有効になり、FALSE
に設定すると無効になります。
3. 問題の根源
GoのOSスレッドは、I/O処理を含む様々なゴルーチンを実行します。Windowsの動的優先度昇格が有効な場合、GoのOSスレッドがI/Oを完了するたびにその優先度が引き上げられます。しかし、GoランタイムはすべてのOSスレッドを同等に扱い、特定のOSスレッドが常に高い優先度を持つことを期待していません。むしろ、Goランタイムの内部スケジューラがゴルーチンを効率的にOSスレッドに割り当てることで、全体のパフォーマンスを最適化しようとします。
I/O完了によって特定のGoのOSスレッドの優先度が不必要に引き上げられると、以下のような問題が発生します。
- スケジューリングの不均衡: 優先度が上がったスレッドがCPUを独占しやすくなり、他のGoのOSスレッド(例えば、タイマー処理やガベージコレクションなど、ランタイムの健全な動作に必要なタスクを実行するスレッド)がCPUを獲得しにくくなります。
- レイテンシの増加: 特にユニプロセッサ環境やCPUリソースが限られている環境では、優先度の高いスレッドが他の重要なランタイムタスクの実行を妨げ、タイマーの遅延や全体的な応答性の低下を引き起こす可能性があります。コミットメッセージの例では、タイマースレッドのスケジューリングが2秒以上遅れるという深刻な問題が指摘されています。
このため、Goランタイムの設計思想とWindowsの動的優先度昇格のミスマッチが、Windows上でのGoアプリケーションのパフォーマンスと安定性に悪影響を与えていたのです。
技術的詳細
このコミットは、Windows環境におけるGoランタイムの動作を改善するために、主に以下の2つの技術的な変更を導入しています。
1. Windowsの動的優先度昇格の無効化
最も重要な変更は、Goプロセス全体に対してWindowsの動的優先度昇格を無効にすることです。これは、src/pkg/runtime/os_windows.c
ファイル内の runtime·osinit
関数に以下のコードを追加することで実現されています。
void *kernel32;
void *SetProcessPriorityBoost;
kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");
if(kernel32 != nil) {
// Windows dynamic priority boosting assumes that a process has different types
// of dedicated threads -- GUI, IO, computational, etc. Go processes use
// equivalent threads that all do a mix of GUI, IO, computations, etc.
// In such context dynamic priority boosting does nothing but harm, so we turn it off.
SetProcessPriorityBoost = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "SetProcessPriorityBoost");
if(SetProcessPriorityBoost != nil) // supported since Windows XP
runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1);
}
LoadLibraryA("kernel32.dll")
: まず、Windows API関数を呼び出すために必要なkernel32.dll
ライブラリをロードします。これは、Windowsのコアシステム機能を提供するDLLです。GetProcAddress(kernel32, "SetProcessPriorityBoost")
: ロードしたkernel32.dll
から、SetProcessPriorityBoost
関数のアドレスを取得します。この関数はWindows XP以降でサポートされています。SetProcessPriorityBoost((uintptr)-1, (uintptr)1)
: 取得したSetProcessPriorityBoost
関数を呼び出します。- 最初の引数
(uintptr)-1
は、現在のプロセス(GetCurrentProcess()
の擬似ハンドル)を指します。 - 2番目の引数
(uintptr)1
は、FALSE
を意味します。SetProcessPriorityBoost
の2番目の引数はブール値で、TRUE
(0以外)で昇格を有効にし、FALSE
(0)で無効にします。ここでは1
を渡すことで、動的優先度昇格を無効にしています。- 補足: Windows APIのブール値は通常
0
がFALSE
、0以外
がTRUE
です。しかし、SetProcessPriorityBoost
のドキュメントを見ると、2番目の引数はDisableBoost
という名前で、TRUE
を渡すとブーストを無効にすると記載されています。したがって、1
を渡すことでブーストを無効にしています。
- 補足: Windows APIのブール値は通常
- 最初の引数
この変更により、Goプロセス内のすべてのスレッドは、I/O完了などによって動的に優先度が引き上げられることがなくなり、Goランタイムのスケジューラがより予測可能にスレッドを管理できるようになります。
2. netpoll_windows.c
における wait
パラメータの変更
src/pkg/runtime/netpoll_windows.c
ファイルでは、ネットワークポーリングの待機時間に関する変更が行われています。
--- a/src/pkg/runtime/netpoll_windows.c
+++ b/src/pkg/runtime/netpoll_windows.c
@@ -78,8 +78,7 @@ retry:
qty = 0;
wait = INFINITE;
if(!block)
- // TODO(brainman): should use 0 here instead, but scheduler hogs CPU
- wait = 1;
+ wait = 0;
// TODO(brainman): Need a loop here to fetch all pending notifications
// (or at least a batch). Scheduler will behave better if is given
// a batch of newly runnable goroutines.
- 以前のコードでは、
block
がfalse
の場合(つまり、ブロックせずにポーリングする場合)、wait
変数に1
が設定されていました。これは、ポーリング関数がごく短い時間だけ待機することを意味します。コメントには「0
を使うべきだが、スケジューラがCPUを占有する」とありました。 - このコミットでは、
wait
の値が0
に変更されました。これは、ポーリング関数が全く待機せず、すぐに結果を返すことを意味します。
この変更は、動的優先度昇格の無効化と合わせて、Goランタイムのスケジューラがより細かくCPUリソースを制御できるようにするためのものです。wait = 0
にすることで、ネットワークイベントがない場合にポーリングスレッドがすぐにCPUを解放し、他のゴルーチンやランタイムタスクにCPUを譲る機会が増えます。これにより、CPUの占有を防ぎ、全体的なスケジューリングの効率が向上します。
3. timeout_test.go
からのWindowsスキップの削除
src/pkg/net/timeout_test.go
ファイルでは、Windows環境でのテストスキップが削除されました。
--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -423,8 +423,6 @@ func testVariousDeadlines(t *testing.T, maxProcs int) {
switch runtime.GOOS {\n \tcase \"plan9\":\n \t\tt.Skipf(\"skipping test on %q\", runtime.GOOS)\n-\tcase \"windows\":\n-\t\tt.Skipf(\"skipping test on %q, see issue 5971\", runtime.GOOS)\n \t}\n \n \tdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs))\
- 以前は、Issue 5971に関連する問題のため、Windows上での特定のタイムアウトテストがスキップされていました。
- 今回のコミットでIssue 5971が修正されたため、このスキップが不要となり削除されました。これにより、Windows環境でもこのテストが実行されるようになり、GoランタイムのWindowsサポートの品質が向上します。
これらの変更は、GoランタイムがWindows上でより効率的かつ安定して動作するための重要な改善です。特に、WindowsのOSスケジューリングの特性とGoの並行処理モデルとの間のミスマッチを解消することを目的としています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の通りです。
-
src/pkg/runtime/os_windows.c
のruntime·osinit
関数内: Windows APISetProcessPriorityBoost
を呼び出して、プロセスの動的優先度昇格を無効にする部分。// ... (既存のコード) ... void runtime·osinit(void) { void *kernel32; void *SetProcessPriorityBoost; // ... (既存のコード) ... kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll"); if(kernel32 != nil) { // Windows dynamic priority boosting assumes that a process has different types // of dedicated threads -- GUI, IO, computational, etc. Go processes use // equivalent threads that all do a mix of GUI, IO, computations, etc. // In such context dynamic priority boosting does nothing but harm, so we turn it off. SetProcessPriorityBoost = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "SetProcessPriorityBoost"); if(SetProcessPriorityBoost != nil) // supported since Windows XP runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1); } } // ... (既存のコード) ...
-
src/pkg/runtime/netpoll_windows.c
のネットワークポーリングループ内:wait
変数の値を1
から0
に変更する部分。// ... (既存のコード) ... retry: qty = 0; wait = INFINITE; if(!block) // TODO(brainman): should use 0 here instead, but scheduler hogs CPU - wait = 1; + wait = 0; // TODO(brainman): Need a loop here to fetch all pending notifications // (or at least a batch). Scheduler will behave better if is given // a batch of newly runnable goroutines. // ... (既存のコード) ...
-
src/pkg/net/timeout_test.go
のtestVariousDeadlines
関数内: Windowsでのテストスキップを削除する部分。// ... (既存のコード) ... switch runtime.GOOS { case "plan9": t.Skipf("skipping test on %q", runtime.GOOS) - case "windows": - t.Skipf("skipping test on %q, see issue 5971", runtime.GOOS) } defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs)) // ... (既存のコード) ...
コアとなるコードの解説
1. SetProcessPriorityBoost
による動的優先度昇格の無効化
src/pkg/runtime/os_windows.c
の runtime·osinit
関数は、GoランタイムがWindows上で初期化される際に呼び出される重要な関数です。この関数内で、Windows APIの SetProcessPriorityBoost
が呼び出されています。
runtime·LoadLibraryA("kernel32.dll")
: これは、Windowsのシステムライブラリであるkernel32.dll
をロードするためのGoランタイム内部のヘルパー関数です。kernel32.dll
には、プロセスやスレッドの管理、メモリ操作など、基本的なシステム機能を提供する多くのAPIが含まれています。runtime·GetProcAddress(kernel32, "SetProcessPriorityBoost")
: ロードしたkernel32.dll
から、特定の関数(この場合はSetProcessPriorityBoost
)のアドレスを取得します。これにより、GoランタイムはC言語のコードから直接Windows APIを呼び出すことができるようになります。runtime·stdcall(SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1)
: 取得したSetProcessPriorityBoost
関数を呼び出します。runtime·stdcall
は、GoランタイムがWindowsのstdcall
呼び出し規約に従って関数を呼び出すための内部メカニズムです。- 最初の引数
SetProcessPriorityBoost
は、呼び出す関数のポインタです。 - 2番目の引数
2
は、続く引数の数を示します。 (uintptr)-1
は、現在のプロセスを指す擬似ハンドルです。Windows APIでは、GetCurrentProcess()
関数が返す値と同じ意味を持ちます。(uintptr)1
は、DisableBoost
パラメータにTRUE
を渡すことを意味します。SetProcessPriorityBoost
関数のドキュメントによると、このパラメータがTRUE
の場合、システムはプロセスのスレッドの優先度を動的に引き上げなくなります。これにより、Goプロセス内のすべてのスレッドに対して動的優先度昇格が無効になります。
この変更は、GoランタイムがWindowsのスケジューリングメカニズムに過度に影響されることなく、Go自身のスケジューラがゴルーチンをより効率的に管理できるようにするために不可欠です。特に、I/Oバウンドなゴルーチンが頻繁に優先度を上げ、他の重要なランタイムタスク(タイマー、GCなど)の実行を妨げる問題を解決します。
2. netpoll_windows.c
における wait
パラメータの変更
src/pkg/runtime/netpoll_windows.c
は、Windows環境でのネットワークI/Oポーリング(イベント通知)を担当するGoランタイムのコンポーネントです。このファイル内の変更は、ポーリングの待機動作を調整します。
if(!block) wait = 0;
: この行は、ネットワークポーリングがブロックしないモードで実行される場合に、待機時間wait
を0
に設定します。- 以前は
wait = 1
でした。これは、ポーリング関数が1ミリ秒だけ待機することを意味します。コメントには「0
を使うべきだが、スケジューラがCPUを占有する」とありました。これは、wait = 0
にすると、ネットワークイベントがない場合にポーリングスレッドがすぐにCPUを解放し、他のゴルーチンやランタイムタスクにCPUを譲る機会が増えるためです。 - この変更は、動的優先度昇格の無効化と組み合わされることで、Goランタイムのスケジューラがより細かくCPUリソースを制御できるようになります。ポーリングスレッドが不必要にCPUを保持するのを防ぎ、他のゴルーチンがより迅速に実行される機会を増やします。これにより、特にI/O負荷の高い状況での全体的な応答性とスループットが向上することが期待されます。
- 以前は
3. timeout_test.go
からのWindowsスキップの削除
src/pkg/net/timeout_test.go
は、ネットワークタイムアウトに関連するテストケースを含むファイルです。
- 以前のコードでは、
runtime.GOOS
が"windows"
の場合、t.Skipf
を使用して特定のテストがスキップされていました。スキップの理由として「see issue 5971
」と明記されており、これはWindowsの動的優先度昇格に関連する問題が原因でテストが失敗していたことを示唆しています。 - 今回のコミットで、
SetProcessPriorityBoost
を使用して動的優先度昇格が無効化され、関連する問題が解決されたため、このスキップ行が削除されました。これにより、Windows環境でもこのタイムアウトテストが正常に実行されるようになり、GoランタイムのWindowsサポートの堅牢性が確認できるようになりました。
これらのコード変更は、GoランタイムがWindowsのOSスケジューリングの特性をより適切に考慮し、Goの並行処理モデルと調和させるための重要なステップです。これにより、Windows上でのGoアプリケーションのパフォーマンスと安定性が向上します。
関連リンク
- Go CL 12406043: https://golang.org/cl/12406043
- GitHubコミットページ: https://github.com/golang/go/commit/a574822f8044204357236405dca60d1ca5123ab5
参考にした情報源リンク
- Windows API
SetProcessPriorityBoost
のドキュメント (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setprocesspriorityboost - Go言語のスケジューラに関する一般的な情報 (Go公式ドキュメントやブログ記事など)
- Windowsのスケジューリングに関する一般的な情報 (Microsoftの技術ドキュメントなど)
- GoのIssue 5971 (コミットメッセージに記載されているが、直接的な公開リポジトリでの参照は見つからなかったため、コミットメッセージの内容を主要な情報源とした)