[インデックス 15927] ファイルの概要
このコミットは、Go言語のWindowsネットワークポーラーにおけるパフォーマンス問題を修正するための「バンドエイド(一時的な対処)」です。特に、新しいスケジューラとの組み合わせで発生するパフォーマンス低下を改善し、GetQueuedCompletionStatus()
がブロックするタイミングをGoランタイムにヒントとして与えることで、TCP接続の永続的なベンチマークにおいて大幅な性能向上を実現しています。
コミット
commit ea151041102692e52fbce353f12ca73bdc48cad7
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Mar 25 20:57:36 2013 +0400
net: band-aid for windows network poller
Fixes performance of the current windows network poller
with the new scheduler.
Gives runtime a hint when GetQueuedCompletionStatus() will block.
Fixes #5068.
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 4004000 33906 -99.15%
BenchmarkTCP4Persistent-2 21790 17513 -19.63%
BenchmarkTCP4Persistent-4 44760 34270 -23.44%
BenchmarkTCP4Persistent-6 45280 43000 -5.04%
R=golang-dev, alex.brainman, coocood, rsc
CC=golang-dev
https://golang.org/cl/7612045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ea151041102692e52fbce353f12ca73bdc48cad7
元コミット内容
このコミットは、Windows環境におけるGoのネットワークポーラーのパフォーマンスを改善するためのものです。特に、新しいGoスケジューラが導入された際に発生した性能低下に対処しています。GetQueuedCompletionStatus()
というWindows API呼び出しがブロックする可能性があることをGoランタイムに事前に伝えるヒント機構を導入することで、この問題を解決しています。
ベンチマーク結果は以下の通り、特にBenchmarkTCP4Persistent
において劇的な改善が見られます。
BenchmarkTCP4Persistent
: 4004000 ns/op から 33906 ns/op へ (-99.15%)BenchmarkTCP4Persistent-2
: 21790 ns/op から 17513 ns/op へ (-19.63%)BenchmarkTCP4Persistent-4
: 44760 ns/op から 34270 ns/op へ (-23.44%)BenchmarkTCP4Persistent-6
: 45280 ns/op から 43000 ns/op へ (-5.04%)
この変更は、GoのIssue #5068(現在は #3873)に関連しています。
変更の背景
Go言語は、その軽量なゴルーチンと効率的なスケジューラによって高い並行性を実現しています。しかし、特定のOS(この場合はWindows)におけるI/O操作、特にネットワークI/Oの処理は、OS固有のAPIとGoランタイムのスケジューラとの連携が重要になります。
このコミットが作成された当時、Goの新しいスケジューラが導入され、Windowsのネットワークポーラーとの間でパフォーマンス上の非効率性が発生していました。Windowsでは、非同期I/Oの完了を待つためにI/O Completion Ports (IOCP) と GetQueuedCompletionStatus
APIが広く利用されます。Goのネットワークポーラーも内部的にこのAPIを使用して、I/O操作が完了した際にゴルーチンを再開させていました。
問題は、GoランタイムがGetQueuedCompletionStatus
がどれくらいの期間ブロックするかを正確に予測できなかったことにあります。これにより、スケジューラがI/O待ちのゴルーチンに対して不必要なコンテキストスイッチを行ったり、CPUリソースを効率的に利用できなかったりする可能性がありました。特に、GetQueuedCompletionStatus
が無限にブロックする設定(INFINITE
タイムアウト)で使用される場合、ランタイムはI/O完了を待つ間、そのゴルーチンが完全にブロックされていると見なし、他のゴルーチンに切り替える必要がありました。しかし、もしI/Oがすぐに完了する場合、この切り替えはオーバーヘッドとなります。
このコミットは、GetQueuedCompletionStatus
が実際にブロックする前に、ランタイムに「これからブロックする可能性がある」というヒントを与えることで、この非効率性を解消しようとしました。これにより、ランタイムはより賢明なスケジューリング判断を下せるようになり、特にネットワークI/Oが頻繁に発生するシナリオでのパフォーマンスが大幅に改善されました。
前提知識の解説
Go言語のゴルーチンとスケジューラ
Go言語の並行処理の根幹をなすのが「ゴルーチン(goroutine)」です。ゴルーチンはOSのスレッドよりもはるかに軽量な実行単位であり、数百万個を同時に実行することも可能です。Goランタイムには「スケジューラ」が組み込まれており、このスケジューラがゴルーチンをOSスレッドにマッピングし、実行を管理します。
スケジューラは、ゴルーチンがI/O操作などでブロックされる際に、そのゴルーチンを一時停止し、他の実行可能なゴルーチンにCPUを割り当てます。I/O操作が完了すると、スケジューラはブロックされていたゴルーチンを再開させます。この効率的な切り替えがGoの高い並行性を支えています。
Windows I/O Completion Ports (IOCP) と GetQueuedCompletionStatus
Windowsにおける高性能な非同期I/Oのメカニズムとして、I/O Completion Ports (IOCP) があります。IOCPは、複数の非同期I/O操作の完了通知を一元的に処理するための仕組みです。
- 非同期I/O: I/O操作(ファイル読み書き、ネットワーク通信など)を開始した後、その完了を待たずにプログラムの実行を続行できるI/Oモデルです。完了時に通知を受け取ります。
- I/O Completion Port (IOCP): 非同期I/O操作が完了した際に、その完了通知(完了パケット)がキューされる場所です。
GetQueuedCompletionStatus
: IOCPから完了パケットを取得するためのAPI関数です。この関数は、完了パケットが利用可能になるまでブロックすることができます。タイムアウト値を指定することで、指定された時間だけ待機することも、すぐに利用可能なパケットがない場合に即座にリターンすることも可能です。
GoのWindowsネットワークポーラーは、このIOCPとGetQueuedCompletionStatus
を利用して、ネットワークソケットからのデータ受信や送信完了を非同期に待ち受けています。
システムコールとGoランタイム
GoのプログラムがOSの機能(ファイルI/O、ネットワーク通信など)を利用する際には、「システムコール」を発行します。システムコールは、ユーザーモードのプログラムがカーネルモードのOSサービスにアクセスするためのインターフェースです。
Goランタイムは、システムコールがブロックする可能性があることを認識しています。ゴルーチンがブロックするシステムコールを実行する際、Goランタイムは通常、そのゴルーチンをOSスレッドから切り離し、そのOSスレッドを他の実行可能なゴルーチンに再利用します。これにより、システムコールがブロックしている間もCPUリソースが無駄になりません。システムコールが完了すると、ランタイムはゴルーチンを元のOSスレッド(または別の利用可能なOSスレッド)に戻し、実行を再開させます。
技術的詳細
このコミットの核心は、GoランタイムがGetQueuedCompletionStatus
のブロック挙動をより正確に予測し、それに基づいてスケジューリングを最適化する点にあります。
従来のGetQueuedCompletionStatus
の呼び出しは、syscall.INFINITE
というタイムアウト値で設定されていました。これは、完了パケットが利用可能になるまで無限にブロックすることを意味します。この設定では、GoランタイムはI/O操作が完了するまでゴルーチンが完全にブロックされると仮定し、そのゴルーチンが実行されていたOSスレッドを解放して他のゴルーチンに割り当てていました。しかし、もしI/Oが非常に短時間で完了する場合、このスレッドの解放と再割り当てのオーバーヘッドがパフォーマンスに悪影響を与える可能性がありました。
このコミットでは、この問題を解決するために以下の戦略が導入されました。
- 非ブロック呼び出しの試行:
src/pkg/net/fd_windows.go
内のresultSrv.Run()
関数において、syscall.GetQueuedCompletionStatus
の呼び出しが最初にタイムアウト値0
で試行されるようになりました。これは、完了パケットがすぐに利用可能であればそれを取得し、そうでなければ即座にWAIT_TIMEOUT
エラーを返すことを意味します。 - ブロックヒントの提供: もし最初の非ブロック呼び出しが
WAIT_TIMEOUT
エラーを返し、かつo
(Overlapped構造体へのポインタ)がnil
である場合(これは、完了パケットがまだキューされていないことを示唆します)、runtime_blockingSyscallHint()
という新しい関数が呼び出されます。 - 無限ブロック呼び出しへの移行:
runtime_blockingSyscallHint()
が呼び出された後、syscall.GetQueuedCompletionStatus
は再びsyscall.INFINITE
タイムアウトで呼び出されます。これにより、実際にブロックが必要な場合にのみ無限ブロック待機が行われます。 - ランタイムへのヒント伝達:
runtime_blockingSyscallHint()
関数は、現在のゴルーチン(g
)の構造体に新しく追加されたblockingsyscall
というブール値をtrue
に設定します。 - スケジューラの最適化:
src/pkg/runtime/cgocall.c
内のruntime·cgocall
関数(CGO呼び出し、つまりGoからC/C++コードを呼び出す際のラッパー)において、システムコールに入る直前にg->blockingsyscall
の値がチェックされます。- もし
blockingsyscall
がtrue
であれば、runtime·entersyscallblock()
が呼び出されます。この関数は、システムコールがブロックすることが予想されるため、Goスケジューラに対して、現在のゴルーチンがブロックされることを明示的に通知し、OSスレッドを他のゴルーチンに再利用する準備を促します。blockingsyscall
フラグはその後false
にリセットされます。 blockingsyscall
がfalse
であれば、通常のruntime·entersyscall()
が呼び出されます。これは、システムコールがブロックしないか、ブロックしても非常に短時間であると予想される場合に用いられます。
- もし
このメカニズムにより、GoランタイムはGetQueuedCompletionStatus
が実際にブロックする可能性が高い場合にのみ、よりコストのかかるentersyscallblock
パス(OSスレッドの解放と再利用)を選択できるようになります。I/Oがすぐに完了する場合は、非ブロック呼び出しで即座に処理し、スケジューリングオーバーヘッドを回避します。これにより、特に高頻度でI/Oが発生するネットワークアプリケーションにおいて、コンテキストスイッチの最適化とCPU利用率の向上が図られ、ベンチマーク結果に見られるような大幅なパフォーマンス改善が実現されました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下の3つのファイルにわたります。
-
src/pkg/net/fd_windows.go
:runtime_blockingSyscallHint()
関数の前方宣言が追加されました。resultSrv.Run()
関数内で、syscall.GetQueuedCompletionStatus
の呼び出しロジックが変更されました。- 最初の呼び出しが
syscall.INFINITE
から0
(非ブロック)に変更。 WAIT_TIMEOUT
エラーかつo == nil
の場合にruntime_blockingSyscallHint()
を呼び出し、その後syscall.INFINITE
で再試行するロジックが追加。
- 最初の呼び出しが
-
src/pkg/runtime/cgocall.c
:net·runtime_blockingSyscallHint(void)
関数が追加されました。この関数は、現在のゴルーチン(g
)のblockingsyscall
フィールドをtrue
に設定します。runtime·cgocall
関数内で、システムコールに入る前の処理が変更されました。g->blockingsyscall
がtrue
の場合にruntime·entersyscallblock()
を呼び出し、そうでなければruntime·entersyscall()
を呼び出す条件分岐が追加されました。
-
src/pkg/runtime/runtime.h
:struct G
(ゴルーチン構造体)に新しいフィールドbool blockingsyscall;
が追加されました。これは、次のシステムコールがブロックする可能性があることを示すヒントとして使用されます。
コアとなるコードの解説
src/pkg/net/fd_windows.go
func runtime_blockingSyscallHint()
func (s *resultSrv) Run() {
var o *syscall.Overlapped
var key uint32
var r ioResult
for {
// 最初の試行は非ブロック (タイムアウト 0) で行う
r.err = syscall.GetQueuedCompletionStatus(s.iocp, &(r.qty), &key, &o, 0)
// もしタイムアウトし、かつ完了パケットがまだない場合 (o == nil)
if r.err == syscall.Errno(syscall.WAIT_TIMEOUT) && o == nil {
// ランタイムに次のシステムコールがブロックするヒントを与える
runtime_blockingSyscallHint()
// その後、無限にブロックする呼び出しを行う
r.err = syscall.GetQueuedCompletionStatus(s.iocp, &(r.qty), &key, &o, syscall.INFINITE)
}
switch {
case r.err == nil:
// 正常に完了したIOパケットをデキュー
// ... (後続の処理)
}
}
}
この変更は、WindowsのネットワークポーラーがIOCPから完了パケットを取得する際の戦略を根本的に変えています。まず非ブロックで試行することで、I/Oが既に完了している場合にすぐに処理を進め、無駄なスケジューリングオーバーヘッドを避けます。もしI/Oがまだ完了していない(WAIT_TIMEOUT
)場合は、runtime_blockingSyscallHint()
を呼び出してランタイムに「これからブロックするシステムコールに入る」というヒントを与え、その後で無限ブロック待機に入ります。これにより、ランタイムはブロックが予想される場合にのみ、より積極的なスケジューリング最適化を行うことができます。
src/pkg/runtime/cgocall.c
// Gives a hint that the next syscall
// executed by the current goroutine will block.
// Currently used only on windows.
void
net·runtime_blockingSyscallHint(void)
{
g->blockingsyscall = true;
}
void
runtime·cgocall(void (*fn)(void*), void *arg)
{
// ... (省略)
/*
* The goroutine is about to enter a system call.
* We need to tell the scheduler about this,
* so it is safe to call while "in a system call", outside
* the $GOMAXPROCS accounting.
*/
if(g->blockingsyscall) {
g->blockingsyscall = false; // フラグをリセット
runtime·entersyscallblock(); // ブロックするシステムコールとして通知
} else
runtime·entersyscall(); // 通常のシステムコールとして通知
runtime·asmcgocall(fn, arg);
runtime·exitsyscall();
}
net·runtime_blockingSyscallHint
関数は非常にシンプルで、現在のゴルーチン構造体g
のblockingsyscall
フラグをtrue
に設定するだけです。このフラグは、runtime·cgocall
内でシステムコールに入る直前にチェックされます。もしblockingsyscall
がtrue
であれば、runtime·entersyscallblock()
が呼び出され、Goスケジューラにこのシステムコールがブロックする可能性が高いことを伝えます。これにより、スケジューラは現在のOSスレッドを他のゴルーチンに割り当てるなどの最適化を行うことができます。フラグは一度使用されたらfalse
にリセットされます。
src/pkg/runtime/runtime.h
struct G
{
// ... (省略)
bool ispanic;
bool issystem; // do not output in stack dump
bool isbackground; // ignore in deadlock detector
bool blockingsyscall; // hint that the next syscall will block
int8 traceignore; // ignore race detection events
// ... (省略)
};
ゴルーチン構造体G
にblockingsyscall
という新しいフィールドが追加されました。このフィールドは、net/fd_windows.go
から設定され、runtime/cgocall.c
で参照されることで、Goランタイムのスケジューラにシステムコールのブロック挙動に関するヒントを伝達する役割を担います。
関連リンク
- Go Issue #5068 (現在は #3873): https://github.com/golang/go/issues/3873 (このコミットが修正したとされる元のIssue)
- Go Change List 7612045: https://golang.org/cl/7612045 (このコミットの元の変更リスト)
参考にした情報源リンク
- Go言語のスケジューラに関する一般的な情報:
- The Go scheduler: https://go.dev/doc/go1.2#scheduler
- Go's work-stealing scheduler: https://go.dev/blog/go1.2scheduler
- Windows I/O Completion Ports (IOCP) および
GetQueuedCompletionStatus
に関する情報:- I/O Completion Ports (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports
- GetQueuedCompletionStatus function (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus
- GoのネットワークポーラーとIOCPの関連性に関する情報:
- Go runtime's network poller: https://go.dev/src/runtime/netpoll.go (Goのソースコード)
- Go's use of IOCP on Windows: https://go.dev/src/runtime/netpoll_windows.go (Goのソースコード)
- 検索結果で参照された情報源:
- github.com (Go issue 5068): https://github.com/golang/go/issues/3873
- go.dev (Go runtime's network poller): https://go.dev/blog/go1.2scheduler (スケジューラに関するブログ記事だが、I/O待ちのゴルーチンに関する言及がある)
- stackoverflow.com (IOCP scalability): https://stackoverflow.com/questions/100000/iocp-scalability