[インデックス 16837] ファイルの概要
このコミットは、Go言語のネットワークポーラー(netpoll)の実装をWindows向けに大幅に改善し、そのロジックをnetパッケージ(ユーザーランド)からruntimeパッケージ(Goランタイム)へ移動させるものです。これにより、Windows上でのネットワークI/Oの効率が向上し、特にTCP通信のパフォーマンスが大幅に改善されています。ベンチマーク結果が示すように、様々なTCP操作において最大で約40%の性能向上が見られます。
コミット
commit 6ea7bf253c67ab85a4ba99bb1716112ca139de9f
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Mon Jul 22 12:49:57 2013 +1000
net: implement netpoll for windows
Moves the network poller from net package into runtime.
benchmark old ns/op new ns/op delta
BenchmarkTCP4OneShot 316386 287061 -9.27%
BenchmarkTCP4OneShot-2 339822 313424 -7.77%
BenchmarkTCP4OneShot-3 330057 306589 -7.11%
BenchmarkTCP4OneShotTimeout 341775 287061 -16.01%
BenchmarkTCP4OneShotTimeout-2 380835 295849 -22.32%
BenchmarkTCP4OneShotTimeout-3 398412 328070 -17.66%
BenchmarkTCP4Persistent 40622 33392 -17.80%
BenchmarkTCP4Persistent-2 44528 35736 -19.74%
BenchmarkTCP4Persistent-3 44919 36907 -17.84%
BenchmarkTCP4PersistentTimeout 45309 33588 -25.87%
BenchmarkTCP4PersistentTimeout-2 50289 38079 -24.28%
BenchmarkTCP4PersistentTimeout-3 51559 37103 -28.04%
BenchmarkTCP6OneShot 361305 345645 -4.33%
BenchmarkTCP6OneShot-2 361305 331976 -8.12%
BenchmarkTCP6OneShot-3 376929 347598 -7.78%
BenchmarkTCP6OneShotTimeout 361305 322212 -10.82%
BenchmarkTCP6OneShotTimeout-2 378882 333928 -11.86%
BenchmarkTCP6OneShotTimeout-3 388647 335881 -13.58%
BenchmarkTCP6Persistent 47653 35345 -25.83%
BenchmarkTCP6Persistent-2 49215 35736 -27.39%
BenchmarkTCP6Persistent-3 38474 37493 -2.55%
BenchmarkTCP6PersistentTimeout 56637 34369 -39.32%
BenchmarkTCP6PersistentTimeout-2 42575 38079 -10.56%
BenchmarkTCP6PersistentTimeout-3 44137 37689 -14.61%
R=dvyukov
CC=golang-dev
https://golang.org/cl/8670044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6ea7bf253c67ab85a4ba99bb1716112ca139de9f
元コミット内容
このコミットの元々の目的は、WindowsにおけるGoのネットワークI/O処理の効率を向上させることでした。具体的には、ネットワークポーラーの機能をnetパッケージからruntimeパッケージへ移管し、Windowsのネイティブな非同期I/OメカニズムであるI/O Completion Ports (IOCP) をGoランタイムが直接管理するように変更することで、より低レイヤーでの最適化とパフォーマンス改善を目指しました。
変更の背景
Go言語は、その並行処理モデルと軽量なゴルーチンによって高いネットワークI/O性能を実現することを目指しています。しかし、OS固有のI/O多重化メカニズム(Linux/Unix系ではepoll/kqueue、WindowsではIOCP)を効率的に利用することは、その性能を最大限に引き出す上で不可欠です。
このコミット以前のWindowsにおけるGoのネットワークI/Oは、netパッケージ内でIOCPを間接的に利用していましたが、Goランタイムのスケジューラとの連携が最適ではありませんでした。特に、ネットワークI/Oが完了するまでゴルーチンがブロックされる際に、ランタイムがそのゴルーチンを効率的にサスペンドし、I/O完了時に再開するメカニズムが洗練されていませんでした。
この非効率性は、特に高負荷なネットワークアプリケーションにおいて顕著なパフォーマンスボトルネックとなっていました。そのため、ネットワークポーラーのロジックをGoランタイムに直接組み込むことで、ランタイムスケジューラがI/Oイベントをより直接的に検知し、ゴルーチンのスケジューリングを最適化できるようになります。これにより、I/O待ちによるコンテキストスイッチのオーバーヘッドを削減し、全体的なスループットとレイテンシを改善することが期待されました。
前提知識の解説
このコミットを理解するためには、以下の概念を把握しておく必要があります。
1. Goランタイムとゴルーチン (Goroutines)
Go言語のプログラムは、Goランタイムによって管理されます。Goランタイムは、軽量な並行処理単位であるゴルーチンをスケジューリングします。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万のゴルーチンを同時に実行することが可能です。Goランタイムのスケジューラは、これらのゴルーチンをOSスレッドにマッピングし、効率的に実行します。
2. ネットワークポーラー (Netpoller)
ネットワークポーラーは、Goランタイムの一部であり、ネットワークI/O操作の完了を監視するメカニズムです。ゴルーチンがネットワークI/O(例: ソケットからの読み書き)を実行する際、その操作が完了するまでブロックされる可能性があります。ネットワークポーラーは、このようなブロックを非同期的に処理し、I/Oが完了した際に該当するゴルーチンを「実行可能」状態に戻し、スケジューラがそのゴルーチンを再開できるようにします。これにより、I/O待ちの間もOSスレッドを他のゴルーチンに利用させることができ、リソースの有効活用と高い並行性を実現します。
3. I/O Completion Ports (IOCP)
IOCPは、Windowsオペレーティングシステムが提供する高性能な非同期I/Oメカニズムです。これは、複数の非同期I/O操作の完了イベントを効率的に処理するために設計されています。
- 仕組み: アプリケーションは、ファイルハンドル(ソケットも含む)をIOCPに関連付けます。I/O操作を開始する際、
OVERLAPPED構造体を渡します。I/O操作が完了すると、その結果はIOCPのキューにポストされます。 - 利点:
- スケーラビリティ: 多数のI/O操作を少数のスレッドで効率的に処理できます。各I/O操作に対して専用のスレッドを割り当てる必要がありません。
- 効率性: I/O完了イベントを待機するスレッドは、イベントが発生するまでCPUリソースを消費しません。イベントが発生すると、OSは待機中のスレッドのいずれかを起こし、完了したI/O操作の情報を渡します。
- 並行性: 複数のスレッドが同じIOCPから同時に完了イベントを取得できます。これにより、マルチコアプロセッサの恩恵を最大限に活用できます。
4. netパッケージとruntimeパッケージ
netパッケージ: Goの標準ライブラリの一部で、ネットワーク通信(TCP/UDPソケット、HTTPクライアント/サーバーなど)のための高レベルなAPIを提供します。runtimeパッケージ: Goランタイムのコア機能(ゴルーチン管理、ガベージコレクション、スケジューリング、OSとのインタラクションなど)を実装しています。
このコミットの変更は、netパッケージが間接的に行っていたIOCPとの連携を、runtimeパッケージが直接行うようにすることで、GoのスケジューラとWindowsのIOCPがより密接に連携し、I/O処理のオーバーヘッドを削減することを目指しています。
技術的詳細
このコミットの主要な技術的変更点は、WindowsにおけるネットワークI/Oの処理フローをnetパッケージからruntimeパッケージへ移行し、IOCPの利用を最適化したことです。
1. netpoll_windows.cの新規追加
最も重要な変更は、src/pkg/runtime/netpoll_windows.cという新しいファイルが追加されたことです。このC言語で書かれたファイルは、WindowsのIOCP APIを直接呼び出し、Goランタイムのネットワークポーラーとして機能します。
runtime·netpollinit(): IOCPハンドルを作成し、初期化します。これはGoプログラムの起動時に一度だけ呼び出されます。runtime·netpollopen(): 新しいネットワークファイルディスクリプタ(ソケット)が作成される際に、そのソケットをIOCPに関連付けます。これにより、そのソケットに対する非同期I/O操作の完了イベントがIOCPにポストされるようになります。runtime·netpoll(): Goランタイムのスケジューラから定期的に呼び出される関数で、IOCPから完了したI/Oイベントを取得します。GetQueuedCompletionStatus関数を使用して、IOCPキューから完了したI/O操作の結果を待ちます。block引数によって、ブロックして待機するか、非ブロックでポーリングするかが制御されます。- I/Oが完了すると、関連付けられた
net_anOp構造体(後述)からruntimeCtx(GoのPollDesc構造体へのポインタ)とmode(読み込みまたは書き込み)を取得し、runtime·netpollready()を呼び出して、対応するゴルーチンを実行可能状態にします。
2. net.anOp構造体の変更とruntime.net_anOpの導入
以前はnetパッケージ内のanOp構造体がI/O操作のコンテキストを保持し、ioResultチャネルを通じて結果を待機していました。このコミットでは、anOp構造体にGoランタイムが利用するフィールドが追加されました。
runtimeCtx uintptr: GoランタイムのPollDesc構造体へのポインタを保持します。これにより、CコードからGoランタイムのポーリング記述子にアクセスできるようになります。mode int32: I/O操作のモード(読み込みまたは書き込み)を示します。errno int32: I/O操作が失敗した場合のエラーコードを保持します。qty uint32: 転送されたバイト数を保持します。
また、runtime/netpoll_windows.cでは、net.anOpと互換性のあるC言語の構造体net_anOpが定義され、IOCPのOVERLAPPED構造体とGoランタイムが利用するフィールドが先頭に配置されています。これは、IOCPが完了イベントを通知する際にOVERLAPPED構造体へのポインタを返すため、そのポインタをnet_anOpとしてキャストして利用できるようにするためです。
3. net.netFDとpollDescの連携
net.netFD構造体は、ネットワークファイルディスクリプタ(ソケット)を表します。このコミットでは、netFDにpd pollDescフィールドが追加されました。
pollDescはGoランタイムのruntime.pollDesc構造体であり、各ネットワークI/Oオブジェクトの状態(読み込み/書き込みの準備ができているか、タイムアウト、クローズなど)を管理します。netFD.Init()やnewFD()関数内で、netfd.pd.Init(netfd)が呼び出され、netFDとpollDescが関連付けられます。ExecIO関数は、I/O操作を開始する前にo.fd.pd.Prepare(int(o.mode))を呼び出し、ランタイムにI/O操作の準備ができたことを通知します。- I/O操作の完了を待つ際には、
o.fd.pd.Wait(int(o.mode))が呼び出され、ランタイムのネットワークポーラーがI/O完了イベントを待機します。これにより、以前のselect文によるチャネル待機やタイマー管理が不要になり、ランタイムが直接I/Oイベントを処理できるようになりました。
4. ExecIO関数の簡素化と効率化
src/pkg/net/fd_windows.goのExecIO関数は、ネットワークI/O操作を実行する中心的な関数です。このコミットにより、ExecIOは大幅に簡素化され、効率が向上しました。
- タイムアウト処理の削除: 以前は
ExecIO内でtime.NewTimerを使用してI/Oタイムアウトを管理していましたが、これが削除されました。タイムアウト処理はGoランタイムのpollDescが担当するようになり、より統合された形で処理されます。 resultcチャネルの削除: I/O完了結果を待機するためのioResult構造体とresultcチャネルが削除されました。I/O完了イベントは直接ランタイムのnetpollによって処理され、結果はanOp構造体のフィールドに直接書き込まれます。runtime.netpollとの連携:ExecIOは、I/O操作の開始時にpd.Prepare()を呼び出し、完了を待つ際にpd.Wait()を呼び出すことで、Goランタイムのネットワークポーラーと直接連携するようになりました。これにより、I/O完了時のゴルーチンの再開がより効率的に行われます。
5. sockopt_windows.goからのデッドライン設定関数の削除
src/pkg/net/sockopt_windows.goからsetReadDeadline, setWriteDeadline, setDeadline関数が削除されました。これは、I/Oデッドライン(タイムアウト)の管理がnetFD内のpollDescに一元化されたため、これらの関数が不要になったためです。
6. ビルドタグの変更
src/pkg/net/fd_poll_runtime.goとsrc/pkg/runtime/netpoll.gocのビルドタグにwindowsが追加され、これらのファイルがWindowsビルドに含まれるようになりました。src/pkg/runtime/netpoll_stub.cのビルドタグからwindowsが削除され、Windowsではこのスタブ実装ではなく、新しいnetpoll_windows.cが使用されるようになりました。
これらの変更により、WindowsにおけるGoのネットワークI/Oは、OSのネイティブな非同期I/OメカニズムであるIOCPをGoランタイムが直接、かつ効率的に利用するようになり、パフォーマンスが大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
src/pkg/net/fd_windows.go:anOp構造体の変更:resultcチャネルの削除、runtimeCtx,mode,errno,qtyフィールドの追加。ioResult構造体とresultSrvの削除。ExecIO関数のロジック変更: タイムアウト処理の削除、pd.Prepare()とpd.Wait()の利用によるランタイムとの連携。netFD構造体へのpd pollDescフィールドの追加。newFD関数でのnetfd.pd.Init()呼び出し。Close()関数でのfd.pd.Evict()呼び出し。Read,ReadFrom,Write,WriteTo,connect,acceptなどのI/O操作関数におけるiosrv.ExecIOの呼び出しからdeadline引数の削除。
-
src/pkg/runtime/netpoll_windows.c(新規ファイル):- WindowsのIOCP API (
CreateIoCompletionPort,GetQueuedCompletionStatus) を直接利用したネットワークポーラーの実装。 runtime·netpollinit(),runtime·netpollopen(),runtime·netpollclose(),runtime·netpoll()関数の定義。net_anOp構造体の定義(net.anOpと互換性を持たせるため)。
- WindowsのIOCP API (
-
src/pkg/runtime/defs_windows.goおよびsrc/pkg/runtime/defs_windows_*.h:INFINITE,WAIT_TIMEOUT定数の追加。Overlapped型の追加と、対応するCヘッダーファイルでのOVERLAPPED構造体の定義。
これらの変更が、WindowsにおけるGoのネットワークI/Oのパフォーマンス向上に直接寄与しています。
コアとなるコードの解説
src/pkg/net/fd_windows.go の anOp 構造体と ExecIO 関数
// anOp implements functionality common to all IO operations.
// Its beginning must be the same as runtime.net_anOp. Keep these in sync.
type anOp struct {
// Used by IOCP interface, it must be first field
// of the struct, as our code rely on it.
o syscall.Overlapped
// fields used by runtime.netpoll
runtimeCtx uintptr
mode int32
errno int32
qty uint32
errnoc chan error
fd *netFD
}
// ...
func (s *ioSrv) ExecIO(oi anOpIface) (int, error) {
var err error
o := oi.Op()
// Notify runtime netpoll about starting IO.
err = o.fd.pd.Prepare(int(o.mode))
if err != nil {
return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, err}
}
// Start IO.
if canCancelIO {
err = oi.Submit()
} else {
s.submchan <- oi
err = <-o.errnoc
}
if err != nil {
o.fd.pd.Cancel() // Cancel the runtime poller if submit failed
return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, err}
}
// Wait for our request to complete.
err = o.fd.pd.Wait(int(o.mode))
if err == nil {
// All is good. Extract our IO results and return.
if o.errno != 0 {
err = syscall.Errno(o.errno)
return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, err}
}
return int(o.qty), nil
}
// IO is interrupted by "close" or "timeout"
netpollErr := err
switch netpollErr {
case errClosing, errTimeout:
// will deal with those.
default:
panic("net: unexpected runtime.netpoll error: " + netpollErr.Error())
}
// Cancel our request.
if canCancelIO {
err := syscall.CancelIoEx(syscall.Handle(o.Op().fd.sysfd), &o.o)
// Assuming ERROR_NOT_FOUND is returned, if IO is completed.
if err != nil && err != syscall.ERROR_NOT_FOUND {
// TODO(brainman): maybe do something else, but panic.
panic(err)
}
} else {
s.canchan <- oi
<-o.errnoc
}
// Wait for cancellation to complete.
o.fd.pd.WaitCanceled(int(o.mode))
if o.errno != 0 {
err = syscall.Errno(o.errno)
if err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
err = netpollErr
}
return 0, &OpError{oi.Name(), o.fd.net, o.fd.laddr, err}
}
// We issued cancellation request. But, it seems, IO operation succeeded
// before cancellation request run. We need to treat IO operation as
// succeeded (the bytes are actually sent/recv from network).
return int(o.qty), nil
}
anOp構造体: この構造体は、個々の非同期I/O操作のコンテキストを保持します。特に重要なのは、syscall.Overlappedフィールドが先頭に配置されている点です。これは、WindowsのIOCPが完了イベントを通知する際に、このOVERLAPPED構造体へのポインタを返すため、Go側でそのポインタをanOp型にキャストして利用できるようにするためです。runtimeCtx,mode,errno,qtyといったフィールドは、Goランタイムのネットワークポーラー(netpoll_windows.c)がI/O完了時に直接書き込むためのものです。これにより、チャネルを介した結果の受け渡しが不要になり、オーバーヘッドが削減されます。
ExecIO関数: この関数は、ネットワークI/O操作(読み込み、書き込み、接続など)を実行するGo側のエントリポイントです。o.fd.pd.Prepare(int(o.mode)): I/O操作を開始する前に、GoランタイムのpollDescにI/O操作の準備ができたことを通知します。これにより、ランタイムはI/Oが完了するまでゴルーチンをブロック状態にすることができます。oi.Submit(): 実際の非同期I/O操作(例:WSARecv,WSASend)をOSに発行します。o.fd.pd.Wait(int(o.mode)): I/O操作の完了を待ちます。この呼び出しは、Goランタイムのネットワークポーラー(runtime.netpoll)がIOCPから対応する完了イベントを受け取るまで、現在のゴルーチンをブロックします。I/Oが完了すると、netpollがanOp構造体のerrnoとqtyフィールドを更新し、ゴルーチンを再開します。- エラー処理とキャンセル処理: I/Oがタイムアウトしたり、ソケットがクローズされたりした場合のキャンセルロジックも、
pd.WaitCanceled()などを利用してランタイムと連携するように変更されています。
src/pkg/runtime/netpoll_windows.c の runtime·netpoll 関数
// Polls for completed network IO.
// Returns list of goroutines that become runnable.
G*
runtime·netpoll(bool block)
{
uint32 wait, qty, key;
int32 mode, errno;
net_anOp *o;
G *gp;
if(iocphandle == INVALID_HANDLE_VALUE)
return nil;
o = nil;
errno = 0;
qty = 0;
wait = INFINITE;
if(!block)
// TODO(brainman): should use 0 here instead, but scheduler hogs CPU
wait = 1;
// 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.
// TODO(brainman): Call GetQueuedCompletionStatusEx() here when possible.
if(runtime·stdcall(runtime·GetQueuedCompletionStatus, 5, iocphandle, &qty, &key, &o, (uintptr)wait) == 0) {
errno = runtime·getlasterror();
if(o == nil && errno == WAIT_TIMEOUT) {
if(!block)
return nil;
runtime·throw("netpoll: GetQueuedCompletionStatus timed out");
}
if(o == nil) {
runtime·printf("netpoll: GetQueuedCompletionStatus failed (errno=%d)\\n", errno);
runtime·throw("netpoll: GetQueuedCompletionStatus failed");
}
// dequeued failed IO packet, so report that
}
if(o == nil)
runtime·throw("netpoll: GetQueuedCompletionStatus returned o == nil");
mode = o->mode;
if(mode != 'r' && mode != 'w') {
runtime·printf("netpoll: GetQueuedCompletionStatus returned invalid mode=%d\\n", mode);
runtime·throw("netpoll: GetQueuedCompletionStatus returned invalid mode");
}
o->errno = errno;
o->qty = qty;
gp = nil;
runtime·netpollready(&gp, (void*)o->runtimeCtx, mode);
return gp;
}
runtime·netpoll(): この関数は、Goランタイムのスケジューラによって呼び出され、IOCPから完了したI/Oイベントを取得します。runtime·stdcall(runtime·GetQueuedCompletionStatus, ...): Windows APIのGetQueuedCompletionStatusを呼び出し、IOCPキューから完了したI/O操作の結果を取得します。iocphandle: IOCPのハンドル。&qty,&key,&o: 完了したバイト数、完了キー(このコミットでは未使用)、そしてOVERLAPPED構造体へのポインタ(Go側のanOp構造体の先頭アドレス)が返されます。wait: 待機時間。blockがtrueの場合はINFINITE(無限に待機)、falseの場合は1(非ブロックに近いポーリング)が設定されます。
- I/Oが完了すると、
o(net_anOp構造体へのポインタ)を通じて、Go側のanOp構造体のerrnoとqtyフィールドが更新されます。 runtime·netpollready(&gp, (void*)o->runtimeCtx, mode): この重要な関数呼び出しは、Goランタイムの内部関数であり、I/Oが完了したことをランタイムに通知します。o->runtimeCtxは、Go側のPollDesc構造体へのポインタであり、これによりランタイムはどのゴルーチンを再開すべきかを特定できます。modeは、読み込みまたは書き込みのどちらのI/Oが完了したかを示します。この関数が呼び出されることで、I/O待ちでブロックされていたゴルーチンが実行可能状態になり、Goスケジューラによって再開されます。
これらの変更により、GoのネットワークI/Oは、WindowsのIOCPを直接利用し、Goランタイムのスケジューラと密接に連携することで、I/O完了時のゴルーチンの再開が非常に効率的になり、結果としてベンチマークで示されたような大幅なパフォーマンス向上が実現されました。
関連リンク
- Go言語のネットワークI/Oに関する公式ドキュメントやブログ記事(当時のものがあれば)
- Windows I/O Completion Ports (IOCP) のMicrosoft公式ドキュメント
- Goランタイムのスケジューラに関する解説
参考にした情報源リンク
- Go言語のソースコード (golang/go GitHubリポジトリ)
- Go言語のコードレビューシステム (Gerrit)
- Microsoft Docs: I/O Completion Ports
- Go Concurrency Patterns: Pipelines and Cancellation (Goの並行処理モデルの理解に役立つ)
- Go Scheduler: MS-DOS on Steroids (Goスケジューラの詳細な解説)
- The Go netpoller (Goのネットワークポーラーに関する解説記事)
- このコミットの背景にあるGoのネットワークポーラーの概念を理解する上で非常に参考になります。
- Go's net package and the runtime poller (Goのnetパッケージとランタイムポーラーの関係)
- GoのネットワークI/Oがどのようにランタイムと連携しているかを理解するのに役立ちます。
これらの情報源は、GoのネットワークI/O、ランタイム、そしてWindowsのIOCPの仕組みを深く理解するために役立ちます。