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

[インデックス 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.netFDpollDescの連携

net.netFD構造体は、ネットワークファイルディスクリプタ(ソケット)を表します。このコミットでは、netFDpd pollDescフィールドが追加されました。

  • pollDescはGoランタイムのruntime.pollDesc構造体であり、各ネットワークI/Oオブジェクトの状態(読み込み/書き込みの準備ができているか、タイムアウト、クローズなど)を管理します。
  • netFD.Init()newFD()関数内で、netfd.pd.Init(netfd)が呼び出され、netFDpollDescが関連付けられます。
  • 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.goExecIO関数は、ネットワーク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.gosrc/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ランタイムが直接、かつ効率的に利用するようになり、パフォーマンスが大幅に向上しました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. 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引数の削除。
  2. src/pkg/runtime/netpoll_windows.c (新規ファイル):

    • WindowsのIOCP API (CreateIoCompletionPort, GetQueuedCompletionStatus) を直接利用したネットワークポーラーの実装。
    • runtime·netpollinit(), runtime·netpollopen(), runtime·netpollclose(), runtime·netpoll()関数の定義。
    • net_anOp構造体の定義(net.anOpと互換性を持たせるため)。
  3. 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.goanOp 構造体と 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が完了すると、netpollanOp構造体のerrnoqtyフィールドを更新し、ゴルーチンを再開します。
    • エラー処理とキャンセル処理: I/Oがタイムアウトしたり、ソケットがクローズされたりした場合のキャンセルロジックも、pd.WaitCanceled()などを利用してランタイムと連携するように変更されています。

src/pkg/runtime/netpoll_windows.cruntime·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: 待機時間。blocktrueの場合はINFINITE(無限に待機)、falseの場合は1(非ブロックに近いポーリング)が設定されます。
    • I/Oが完了すると、onet_anOp構造体へのポインタ)を通じて、Go側のanOp構造体のerrnoqtyフィールドが更新されます。
    • 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のネットワークI/O、ランタイム、そしてWindowsのIOCPの仕組みを深く理解するために役立ちます。