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

[インデックス 17087] ファイルの概要

このコミットは、Go言語のWindowsネットワークI/O処理におけるパフォーマンス改善を目的としています。具体的には、WindowsのI/O Completion Ports (IOCP) を利用する際に、同期的に完了するI/O操作に対してGetQueuedCompletionStatusの呼び出しをスキップする最適化を導入しています。これにより、特にTCP接続におけるベンチマークで顕著なパフォーマンス向上が見られます。

コミット

commit ed8c5501c743d702016044e10c92e4a211765502
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Aug 8 17:36:43 2013 +0400

    net: use SetFileCompletionNotificationModes on windows if available
    This allows to skip GetQueuedCompletionStatus if an IO operation completes synchronously.
    benchmark                    old ns/op    new ns/op    delta
    BenchmarkTCP4Persistent          27669        25863   -6.53%
    BenchmarkTCP4Persistent-2        18173        15908  -12.46%
    BenchmarkTCP4Persistent-4        10390         9766   -6.01%
    
    R=golang-dev, mikioh.mikioh, alex.brainman
    CC=golang-dev
    https://golang.org/cl/12409044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ed8c5501c743d702016044e10c92e4a211765502

元コミット内容

Windows環境において、ネットワークI/Oのパフォーマンスを向上させるため、利用可能であればSetFileCompletionNotificationModes APIを使用するように変更しました。これにより、I/O操作が同期的に完了した場合にGetQueuedCompletionStatusの呼び出しをスキップできるようになります。ベンチマーク結果では、BenchmarkTCP4Persistentにおいて6%から12%の性能改善が確認されました。

変更の背景

Windowsにおける非同期I/Oの主要なメカニズムはI/O Completion Ports (IOCP) です。IOCPは、複数の非同期I/O操作の結果を効率的に処理するための強力な仕組みですが、I/O操作が即座に完了(同期完了)した場合でも、通常は完了ポートに通知がキューイングされ、GetQueuedCompletionStatusを呼び出してその通知を取得する必要があります。この余分なステップは、特に高頻度で同期完了するI/O操作が発生する場合にオーバーヘッドとなり、パフォーマンスのボトルネックとなる可能性がありました。

このコミットは、このオーバーヘッドを削減し、GoのネットワークスタックがWindows上でより効率的に動作するようにすることを目的としています。SetFileCompletionNotificationModes APIは、特定のI/O操作が同期的に完了した場合に完了ポートへの通知をスキップする機能を提供し、この問題を解決するための直接的な手段となります。

前提知識の解説

I/O Completion Ports (IOCP)

Windowsにおける高性能な非同期I/Oモデルです。サーバーアプリケーションなどで多数の同時接続を効率的に処理するために設計されています。IOCPの基本的な流れは以下の通りです。

  1. 完了ポートの作成: CreateIoCompletionPort関数を使用して完了ポートを作成します。
  2. ファイルハンドルの関連付け: ネットワークソケットやファイルなどのI/Oデバイスのハンドルを完了ポートに関連付けます。
  3. 非同期I/Oの開始: ReadFile, WriteFile, WSARecv, WSASendなどの非同期I/O関数を呼び出します。これらの関数は、I/O操作が完了する前に制御を呼び出し元に戻します。
  4. 完了通知の取得: GetQueuedCompletionStatus関数を呼び出して、完了したI/O操作の通知を待ちます。I/O操作が完了すると、その結果が完了ポートのキューに追加され、GetQueuedCompletionStatusがそれを取得します。

GetQueuedCompletionStatusのオーバーヘッド

前述の通り、I/O操作が即座に完了した場合でも、通常は完了ポートに通知がキューイングされ、GetQueuedCompletionStatusで取得する必要があります。これは、I/Oが実際に非同期で完了した場合と同じパスをたどるため、不要なコンテキストスイッチやキュー操作が発生し、パフォーマンスに影響を与える可能性があります。

SetFileCompletionNotificationModes API

Windows Vista以降で導入されたAPIで、ファイルハンドルに関連付けられたI/O完了通知の動作を制御します。特に重要なフラグは以下の2つです。

  • FILE_SKIP_COMPLETION_PORT_ON_SUCCESS (0x00000001): I/O操作が同期的に成功した場合、完了ポートへの通知をキューイングしないようにします。これにより、GetQueuedCompletionStatusを呼び出す必要がなくなります。
  • FILE_SKIP_SET_EVENT_ON_HANDLE (0x00000002): I/O操作が完了したときに、関連付けられたイベントオブジェクトをシグナル状態に設定しないようにします。Goのネットワークスタックはイベントオブジェクトを使用しないため、このフラグも設定することで不要な処理を削減できます。

IFS (Installable File System) プロバイダ

Windowsのネットワークスタックは、Winsock LSP (Layered Service Provider) やIFSプロバイダなどの拡張メカニズムをサポートしています。IFSプロバイダは、ファイルシステムドライバーのように動作し、ネットワークI/Oをインターセプトして追加の処理を行うことができます。

MicrosoftのKB2568167によると、非IFSプロバイダがインストールされている環境でFILE_SKIP_COMPLETION_PORT_ON_SUCCESSを使用すると、問題が発生する可能性があるとされています。これは、非IFSプロバイダがI/O完了のセマンティクスを正しく処理しない場合があるためです。そのため、この最適化を適用する前に、システムに非IFSプロバイダがインストールされていないことを確認する必要があります。

WSAEnumProtocolsWSAProtocolInfoXP1_IFS_HANDLES

WSAEnumProtocolsは、システムにインストールされているプロトコル(TCP/IPなど)に関する情報を列挙するために使用されるWinsock APIです。このAPIは、各プロトコルに関する詳細情報を含むWSAProtocolInfo構造体の配列を返します。

WSAProtocolInfo構造体には、プロトコルの特性を示す様々なフラグが含まれています。その一つがXP1_IFS_HANDLESです。このフラグは、プロトコルがIFSハンドルをサポートしているかどうかを示します。FILE_SKIP_COMPLETION_PORT_ON_SUCCESSを安全に使用するためには、すべてのインストール済みプロトコルがXP1_IFS_HANDLESフラグを持っていることを確認する必要があります。もし一つでも持っていないプロトコルがあれば、この最適化は適用されません。

TCPとUDPにおけるFILE_SKIP_COMPLETION_PORT_ON_SUCCESSの適用

Microsoftのブログ記事によると、FILE_SKIP_COMPLETION_PORT_ON_SUCCESSはUDPには安全に適用できないとされています。これは、UDPがコネクションレス型プロトコルであり、I/O完了のセマンティクスがTCPとは異なるためです。TCPはコネクション指向であり、I/O操作の完了がより予測可能です。したがって、この最適化はTCP接続に限定して適用されます。

技術的詳細

このコミットは、GoのWindowsネットワークI/O処理において、以下の技術的な変更を導入しています。

  1. SetFileCompletionNotificationModesの動的ロード:
    • src/pkg/syscall/syscall_windows.goLoadSetFileCompletionNotificationModes関数が追加され、kernel32.dllからSetFileCompletionNotificationModes関数のアドレスを動的に取得するようになりました。
    • src/pkg/syscall/zsyscall_windows_386.goおよびsrc/pkg/syscall/zsyscall_windows_amd64.goprocSetFileCompletionNotificationModesが追加され、このAPIのプロシージャエントリポイントが定義されています。
  2. WSAEnumProtocolsの動的ロードとプロトコルチェック:
    • src/pkg/syscall/syscall_windows.goWSAEnumProtocols関数が追加され、ws2_32.dllからWSAEnumProtocolsW関数のアドレスを動的に取得するようになりました。
    • src/pkg/syscall/zsyscall_windows_386.goおよびsrc/pkg/syscall/zsyscall_windows_amd64.goprocWSAEnumProtocolsWが追加されています。
    • src/pkg/syscall/ztypes_windows.goWSAProtocolInfo構造体と関連する定数(FILE_SKIP_COMPLETION_PORT_ON_SUCCESS, FILE_SKIP_SET_EVENT_ON_HANDLE, XP1_IFS_HANDLESなど)が追加されました。
    • src/pkg/net/fd_windows.gosysInit関数内で、SetFileCompletionNotificationModesが利用可能かどうかをチェックし、さらにWSAEnumProtocolsを使用してインストールされているすべてのプロトコルがXP1_IFS_HANDLESフラグを持っているかを確認します。このチェックが成功した場合にのみ、skipSyncNotifフラグがtrueに設定され、同期完了時の通知スキップが有効になります。
  3. I/O操作の最適化:
    • src/pkg/net/fd_windows.gonetFD構造体にskipSyncNotifフィールドが追加されました。これは、特定のネットワークファイルディスクリプタ(ソケット)に対して同期完了時の通知スキップが有効かどうかを示します。
    • netFD.init関数内で、SetFileCompletionNotificationModesが呼び出され、FILE_SKIP_SET_EVENT_ON_HANDLEフラグが常に設定されます。また、skipSyncNotiftrueで、かつTCP接続の場合にのみFILE_SKIP_COMPLETION_PORT_ON_SUCCESSフラグが設定されます。
    • ioSrv.ExecIO関数内で、I/O操作が同期的に完了した場合(err == nil)に、o.fd.skipSyncNotiftrueであれば、GetQueuedCompletionStatusを待たずに即座に結果を返すように変更されました。これにより、不要な完了ポート通知の処理が回避されます。

これらの変更により、GoのネットワークスタックはWindowsのIOCPをより効率的に利用し、特に同期的に完了するI/O操作が多いシナリオでのパフォーマンスを向上させています。

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

  • src/pkg/net/fd_windows.go:
    • sysInit関数にSetFileCompletionNotificationModesWSAEnumProtocolsの利用可能性チェックとプロトコルチェックロジックを追加。
    • netFD構造体にskipSyncNotifフィールドを追加。
    • netFD.init関数でSetFileCompletionNotificationModesを呼び出し、適切なフラグを設定。
    • ioSrv.ExecIO関数で、同期完了時のGetQueuedCompletionStatusスキップロジックを追加。
  • src/pkg/syscall/syscall_windows.go:
    • LoadSetFileCompletionNotificationModes関数とSetFileCompletionNotificationModes関数の宣言を追加。
    • WSAEnumProtocols関数の宣言を追加。
  • src/pkg/syscall/zsyscall_windows_386.go および src/pkg/syscall/zsyscall_windows_amd64.go:
    • procSetFileCompletionNotificationModesprocWSAEnumProtocolsWの定義を追加。
  • src/pkg/syscall/ztypes_windows.go:
    • FILE_SKIP_COMPLETION_PORT_ON_SUCCESSFILE_SKIP_SET_EVENT_ON_HANDLEなどの定数を追加。
    • WSAProtocolInfo構造体と関連する定数(WSAPROTOCOL_LEN, MAX_PROTOCOL_CHAIN, XP1_IFS_HANDLESなど)を追加。

コアとなるコードの解説

src/pkg/net/fd_windows.go

var (
	canCancelIO                               bool // determines if CancelIoEx API is present
	skipSyncNotif                             bool
	hasLoadSetFileCompletionNotificationModes bool
)

func sysInit() {
	// ... 既存の初期化コード ...

	hasLoadSetFileCompletionNotificationModes = syscall.LoadSetFileCompletionNotificationModes() == nil
	if hasLoadSetFileCompletionNotificationModes {
		// It's not safe to use FILE_SKIP_COMPLETION_PORT_ON_SUCCESS if non IFS providers are installed:
		// http://support.microsoft.com/kb/2568167
		skipSyncNotif = true
		protos := [2]int32{syscall.IPPROTO_TCP, 0}
		var buf [32]syscall.WSAProtocolInfo
		len := uint32(unsafe.Sizeof(buf))
		n, err := syscall.WSAEnumProtocols(&protos[0], &buf[0], &len)
		if err != nil {
			skipSyncNotif = false
		} else {
			for i := int32(0); i < n; i++ {
				if buf[i].ServiceFlags1&syscall.XP1_IFS_HANDLES == 0 {
					skipSyncNotif = false
					break
				}
			}
		}
	}
}

// ...

func (s *ioSrv) ExecIO(o *operation, name string, submit func(o *operation) error) (qty int, err error) {
	// ...
	switch err {
	case nil:
		// IO completed immediately
		if o.fd.skipSyncNotif {
			// No completion message will follow, so return immediately.
			return int(o.qty), nil
		}
		// Need to get our completion message anyway.
	case syscall.ERROR_IO_PENDING:
		// IO started, and we have to wait for its completion.
		err = nil
	// ...
	}
	// ...
}

type netFD struct {
	// ...
	sysfd         syscall.Handle
	family        int
	sotype        int
	isConnected   bool
	skipSyncNotif bool // 追加
	net           string
	laddr         Addr
	raddr         Addr
	// ...
}

func (fd *netFD) init() error {
	// ...
	if hasLoadSetFileCompletionNotificationModes {
		// We do not use events, so we can skip them always.
		flags := uint8(syscall.FILE_SKIP_SET_EVENT_ON_HANDLE)
		// It's not safe to skip completion notifications for UDP:
		// http://blogs.technet.com/b/winserverperformance/archive/2008/06/26/designing-applications-for-high-performance-part-iii.aspx
		if skipSyncNotif && fd.net == "tcp" {
			flags |= syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS
		}
		err := syscall.SetFileCompletionNotificationModes(fd.sysfd, flags)
		if err == nil && flags&syscall.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS != 0 {
			fd.skipSyncNotif = true
		}
	}
	// ...
}
  • sysInit関数では、SetFileCompletionNotificationModesが利用可能か(hasLoadSetFileCompletionNotificationModes)をチェックし、さらにWSAEnumProtocolsを使ってシステムにインストールされているプロトコルがすべてIFSハンドルをサポートしているかを確認しています。このチェックが通れば、skipSyncNotiftrueに設定され、同期完了時の通知スキップがシステム全体で有効になる可能性が示されます。
  • ExecIO関数は、I/O操作を実行するGoの内部関数です。I/O操作がエラーなく即座に完了した場合(err == nil)、もしo.fd.skipSyncNotiftrueであれば、完了ポートからの通知を待たずに即座に処理を終えます。これにより、GetQueuedCompletionStatusの呼び出しとそれに伴うオーバーヘッドが削減されます。
  • netFD構造体には、個々のネットワークファイルディスクリプタ(ソケット)が同期完了通知スキップの対象となるかどうかを示すskipSyncNotifフィールドが追加されました。
  • netFD.init関数は、新しいネットワークファイルディスクリプタが初期化される際に呼び出されます。ここで、SetFileCompletionNotificationModesが実際にソケットハンドルに対して呼び出されます。FILE_SKIP_SET_EVENT_ON_HANDLEは常に設定されます。sysInitskipSyncNotiftrueに設定されており、かつ現在のソケットがTCP接続である場合にのみ、FILE_SKIP_COMPLETION_PORT_ON_SUCCESSが設定されます。これにより、UDP接続に対する不適切な最適化が回避されます。

src/pkg/syscall/syscall_windows.go

func LoadSetFileCompletionNotificationModes() error {
	return procSetFileCompletionNotificationModes.Find()
}

func SetFileCompletionNotificationModes(handle Handle, flags uint8) (err error) {
	r1, _, e1 := Syscall(procSetFileCompletionNotificationModes.Addr(), 2, uintptr(handle), uintptr(flags), 0)
	if r1 == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = EINVAL
		}
	}
	return
}

func WSAEnumProtocols(protocols *int32, protocolBuffer *WSAProtocolInfo, bufferLength *uint32) (n int32, err error) {
	r0, _, e1 := Syscall(procWSAEnumProtocolsW.Addr(), 3, uintptr(unsafe.Pointer(protocols)), uintptr(unsafe.Pointer(protocolBuffer)), uintptr(unsafe.Pointer(bufferLength)))
	n = int32(r0)
	if n == -1 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = EINVAL
		}
	}
	return
}

このファイルでは、Windows APIであるSetFileCompletionNotificationModesWSAEnumProtocolsをGoから呼び出すためのラッパー関数が定義されています。Syscall関数を通じて、対応するDLL(kernel32.dllws2_32.dll)内の関数を呼び出します。

src/pkg/syscall/ztypes_windows.go

const (
	FILE_SKIP_COMPLETION_PORT_ON_SUCCESS = 1
	FILE_SKIP_SET_EVENT_ON_HANDLE        = 2
)

// ...

const (
	// ...
	XP1_IFS_HANDLES              = 0x00020000
	// ...
)

type WSAProtocolInfo struct {
	ServiceFlags1     uint32
	// ...
}

このファイルには、Windows APIで使用される定数と構造体のGo言語での定義が含まれています。FILE_SKIP_COMPLETION_PORT_ON_SUCCESSFILE_SKIP_SET_EVENT_ON_HANDLESetFileCompletionNotificationModesに渡すフラグ、XP1_IFS_HANDLESWSAProtocolInfo構造体のServiceFlags1フィールドに含まれるフラグで、IFSプロバイダのサポートを示します。

関連リンク

参考にした情報源リンク