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

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

このコミットは、GoランタイムのWindows版ネットワークポーラーにおいて、I/O完了ポート (IOCP) からの通知取得方法を最適化するものです。具体的には、単一の通知を処理する GetQueuedCompletionStatus の代わりに、複数の通知をバッチで取得できる GetQueuedCompletionStatusEx 関数を利用するように変更し、ネットワークI/Oの効率を向上させています。これにより、特に高負荷なネットワークアプリケーションにおいてパフォーマンスが改善されます。

コミット

commit 65834685d3df3e6219cbf3ab471a13fa997c5b98
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Aug 8 17:41:57 2013 +0400

    runtime: use GetQueuedCompletionStatusEx on windows if available
    GetQueuedCompletionStatusEx allows to dequeue a batch of completion
    notifications, which is more efficient than dequeueing one by one.
    
    benchmark                           old ns/op    new ns/op    delta
    BenchmarkClientServerParallel4         100605        90945   -9.60%
    BenchmarkClientServerParallel4-2        90225        74504  -17.42%
    
    R=golang-dev, alex.brainman
    CC=golang-dev
    https://golang.org/cl/12436044

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

https://github.com/golang/go/commit/65834685d3df3e6219cbf3ab471a13fa997c5b98

元コミット内容

    runtime: use GetQueuedCompletionStatusEx on windows if available
    GetQueuedCompletionStatusEx allows to dequeue a batch of completion
    notifications, which is more efficient than dequeueing one by one.
    
    benchmark                           old ns/op    new ns/op    delta
    BenchmarkClientServerParallel4         100605        90945   -9.60%
    BenchmarkClientServerParallel4-2        90225        74504  -17.42%

変更の背景

Goランタイムのネットワークポーラーは、Windows環境においてI/O完了ポート (IOCP) を利用して非同期I/Oイベントを処理しています。従来の GetQueuedCompletionStatus 関数は、一度に一つの完了通知しか取得できませんでした。これは、特に多数の同時接続や高頻度なI/Oが発生するシナリオにおいて、ポーラーがイベントループ内で何度もシステムコールを発行する必要があるため、オーバーヘッドが大きくなる原因となっていました。

このコミットの背景には、この非効率性を解消し、Goアプリケーションのネットワークパフォーマンスを向上させるという明確な目的があります。Windows Vista以降で利用可能な GetQueuedCompletionStatusEx 関数は、一度の呼び出しで複数の完了通知をバッチで取得する機能を提供します。これにより、システムコール発行回数を削減し、CPU使用率の低下とスループットの向上を実現できるため、この関数を導入することが決定されました。コミットメッセージに記載されているベンチマーク結果は、この変更が実際に顕著なパフォーマンス改善をもたらすことを示しています。

前提知識の解説

I/O完了ポート (IOCP)

I/O完了ポート (IOCP) は、Windowsオペレーティングシステムが提供する高性能な非同期I/Oメカニズムです。主にサーバーアプリケーションや、多数の同時接続を扱う必要があるプログラムで利用されます。IOCPを使用することで、アプリケーションはI/O操作の完了を待つ間に他の処理を実行でき、I/Oが完了した際にシステムから通知を受け取ることができます。

IOCPの主要なコンポーネントは以下の通りです。

  • 完了ポート (Completion Port): CreateIoCompletionPort 関数で作成されるカーネルオブジェクトです。I/O操作が完了すると、関連する完了パケットがこのポートにキューされます。
  • スレッドプール: 完了ポートから完了パケットをデキューし、対応する処理を実行するワーカースレッドのプールです。IOCPは、利用可能なCPUコア数に応じてスレッドの数を自動的に調整し、コンテキストスイッチのオーバーヘッドを最小限に抑えるように設計されています。
  • オーバーラップI/O (Overlapped I/O): 非同期I/O操作を実行するためのメカニズムです。I/O操作を開始する際に OVERLAPPED 構造体を渡すことで、操作の完了を待たずにすぐに制御が呼び出し元に戻ります。操作が完了すると、その結果がIOCPに通知されます。

GetQueuedCompletionStatusGetQueuedCompletionStatusEx

  • GetQueuedCompletionStatus: IOCPから単一の完了パケットをデキューする関数です。I/O操作が完了すると、この関数を呼び出して結果を取得します。キューにパケットがない場合は、パケットが利用可能になるまで呼び出しスレッドはブロックされます(タイムアウトを指定しない場合)。
  • GetQueuedCompletionStatusEx: Windows Vista以降で導入された関数で、GetQueuedCompletionStatus の拡張版です。この関数は、一度の呼び出しで複数の完了パケットを配列としてデキューすることができます。これにより、特にI/O完了イベントが頻繁に発生する環境において、システムコール呼び出しの回数を大幅に削減し、パフォーマンスを向上させることが可能です。バッチ処理により、コンテキストスイッチの回数も減少し、CPU効率が向上します。

OVERLAPPED 構造体

OVERLAPPED 構造体は、Windowsの非同期I/O操作で使用されるデータ構造です。I/O操作の状態(オフセット、転送バイト数など)を保持し、操作が完了した際にシステムがこの構造体を更新します。アプリケーションは、この構造体を通じて非同期I/O操作の進捗と結果を追跡します。

Goの netpoll メカニズム

Goランタイムの netpoll は、ネットワークI/Oの準備ができたことを検出するためのプラットフォーム固有のメカニズムです。Windowsでは、この netpoll がIOCPを利用して実装されています。Goのゴルーチンは、ネットワークI/Oが完了するまでブロックされることなく、他のゴルーチンが実行できるようにスケジューリングされます。I/Oが完了すると、IOCPを通じて netpoll に通知され、対応するゴルーチンが実行可能状態になります。

WSAGetOverlappedResult

WSAGetOverlappedResult は、Winsock (Windows Sockets) APIの一部であり、オーバーラップI/O操作の結果を取得するために使用されます。GetQueuedCompletionStatusGetQueuedCompletionStatusEx が完了パケットをデキューするのに対し、WSAGetOverlappedResult は、特定の OVERLAPPED 構造体に関連付けられたI/O操作の最終的な結果(転送されたバイト数やエラーコードなど)を詳細に取得するために使用されます。

技術的詳細

このコミットの主要な技術的変更点は、GoランタイムのWindowsネットワークポーラー (netpoll_windows.c) が、利用可能であれば GetQueuedCompletionStatusEx を優先的に使用するように修正されたことです。

  1. GetQueuedCompletionStatusEx の動的ロード: src/pkg/runtime/os_windows.c において、runtime·GetQueuedCompletionStatusEx という関数ポインタが追加され、osinit 関数内で GetProcAddress を使用して kernel32.dll から GetQueuedCompletionStatusEx 関数を動的にロードするようになりました。これにより、Windows Vistaより前のOSバージョンでもGoランタイムが動作し続けることができます(その場合は GetQueuedCompletionStatusExnil となり、従来の GetQueuedCompletionStatus がフォールバックとして使用されます)。

  2. OverlappedEntry 構造体の導入: src/pkg/runtime/netpoll_windows.cOverlappedEntry という新しい構造体が定義されました。これは GetQueuedCompletionStatusEx が返す完了パケットの配列要素に対応します。

    typedef struct OverlappedEntry OverlappedEntry;
    struct OverlappedEntry
    {
        uintptr key;
        net_op* op;  // In reality it's Overlapped*, but we cast it to net_op* anyway.
        uintptr internal;
        uint32  qty;
    };
    

    op フィールドは、実際の OVERLAPPED 構造体へのポインタですが、Goランタイム内部の net_op 構造体として扱われます。

  3. netpoll 関数の変更: runtime·netpoll 関数(Goのネットワークポーラーの核心部分)が大幅に修正されました。

    • GetQueuedCompletionStatusEx が利用可能な場合 (runtime·GetQueuedCompletionStatusEx != nil)、entries という OverlappedEntry の配列(サイズは64)を準備し、GetQueuedCompletionStatusEx を呼び出して複数の完了通知を一度に取得します。
    • 取得した各 OverlappedEntry についてループを回し、それぞれの完了パケットを処理します。
    • 各完了パケットに対して、WSAGetOverlappedResult を呼び出して、実際の転送バイト数 (qty) とエラーコード (errno) を取得します。これは、GetQueuedCompletionStatusEx が返す情報だけでは不十分な場合があるためです。
    • 取得した情報 (op, errno, qty) を handlecompletion という新しいヘルパー関数に渡して処理します。
    • GetQueuedCompletionStatusEx が利用できない場合、またはエラーが発生した場合は、従来の GetQueuedCompletionStatus を使用するパスにフォールバックします。
  4. handlecompletion ヘルパー関数の導入: 新しい静的関数 handlecompletion が導入され、完了パケットの共通処理をカプセル化しています。

    static void
    handlecompletion(G **gpp, net_op *op, int32 errno, uint32 qty)
    {
        int32 mode;
    
        if(op == nil)
            runtime·throw("netpoll: GetQueuedCompletionStatus returned op == nil");
        mode = op->mode;
        if(mode != 'r' && mode != 'w') {
            runtime·printf("netpoll: GetQueuedCompletionStatus returned invalid mode=%d\\n", mode);
            runtime·throw("netpoll: GetQueuedCompletionStatus returned invalid mode");
        }
        op->errno = errno;
        op->qty = qty;
        runtime·netpollready(gpp, op->pd, mode);
    }
    

    この関数は、net_op 構造体の errnoqty フィールドを更新し、runtime·netpollready を呼び出して、対応するゴルーチンを実行可能状態にします。これにより、GetQueuedCompletionStatusGetQueuedCompletionStatusEx の両方のパスで共通の完了処理ロジックを再利用できるようになりました。

  5. net_op 構造体の変更: src/pkg/runtime/netpoll_windows.cnet_op 構造体において、uintptr runtimeCtx;PollDesc* pd; に変更されました。これは、PollDesc がポーリング対象のファイルディスクリプタや関連するゴルーチン情報を含む構造体であり、より直接的に関連するデータへのポインタを持つことで、コードの可読性と整合性が向上します。

これらの変更により、GoランタイムはWindows環境でより効率的にネットワークI/Oを処理できるようになり、特に高スループットが求められるアプリケーションにおいて顕著なパフォーマンス改善が期待されます。

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

このコミットで変更された主要なファイルと、その変更の概要は以下の通りです。

  • src/pkg/runtime/netpoll.goc:

    • runtime·netpollfd(PollDesc *pd) という新しい関数が追加されました。これは PollDesc からファイルディスクリプタ (fd) を取得するためのヘルパー関数です。
  • src/pkg/runtime/netpoll_windows.c:

    • GetQueuedCompletionStatusExWSAGetOverlappedResult の動的インポート宣言が追加されました。
    • net_op 構造体の runtimeCtx フィールドが PollDesc* pd に変更されました。
    • OverlappedEntry 構造体が新しく定義されました。
    • handlecompletion という静的ヘルパー関数が追加され、完了通知の共通処理をカプセル化しました。
    • runtime·netpoll 関数が大幅に修正され、GetQueuedCompletionStatusEx を使用するロジックが追加されました。これにより、複数の完了通知をバッチで処理できるようになりました。また、WSAGetOverlappedResult を使用して詳細なI/O結果を取得するようになりました。
  • src/pkg/runtime/os_windows.c:

    • runtime·GetQueuedCompletionStatusEx 関数ポインタが追加されました。
    • runtime·osinit 関数内で、GetProcAddress を使用して GetQueuedCompletionStatusEx 関数を動的にロードする処理が追加されました。
  • src/pkg/runtime/os_windows.h:

    • runtime·GetQueuedCompletionStatusEx の外部宣言が追加されました。
  • src/pkg/runtime/runtime.h:

    • runtime·netpollfd(PollDesc*) 関数のプロトタイプ宣言が追加されました。

コアとなるコードの解説

src/pkg/runtime/netpoll_windows.c の変更点

このファイルはWindowsにおけるネットワークポーラーの実装です。

// 新しいインポート宣言
#pragma dynimport runtime·WSAGetOverlappedResult WSAGetOverlappedResult "ws2_32.dll"
extern void *runtime·WSAGetOverlappedResult;

// OverlappedEntry 構造体の定義
typedef struct OverlappedEntry OverlappedEntry;
struct OverlappedEntry
{
    uintptr key;
    net_op* op;  // In reality it's Overlapped*, but we cast it to net_op* anyway.
    uintptr internal;
    uint32  qty;
};

// handlecompletion ヘルパー関数のプロトタイプ宣言
static void handlecompletion(G **gpp, net_op *o, int32 errno, uint32 qty);

// net_op 構造体の変更
struct net_op
{
    // used by windows
    Overlapped o;
    // used by netpoll
    PollDesc*   pd; // 変更点: uintptr runtimeCtx; から PollDesc* pd; へ
    int32   mode;
    int32   errno;
    uint32  qty;
};

// runtime·netpoll 関数の主要な変更部分
G*
runtime·netpoll(bool block)
{
    OverlappedEntry entries[64]; // 複数の完了通知を格納する配列
    uint32 wait, qty, key, flags, n, i;
    int32 errno;
    net_op *op;
    G *gp;

    // ... (初期化コード) ...

    if(runtime·GetQueuedCompletionStatusEx != nil) { // GetQueuedCompletionStatusEx が利用可能かチェック
        n = nelem(entries) / runtime·gomaxprocs; // 取得するエントリ数をゴルーチン数に応じて調整
        if(n < 8)
            n = 8;
        // GetQueuedCompletionStatusEx を呼び出し、複数の完了通知をバッチで取得
        if(runtime·stdcall(runtime·GetQueuedCompletionStatusEx, 6, iocphandle, entries, (uintptr)n, &n, (uintptr)wait, (uintptr)0) == 0) {
            errno = runtime·getlasterror();
            if(!block && errno == WAIT_TIMEOUT)
                return nil;
            runtime·printf("netpoll: GetQueuedCompletionStatusEx failed (errno=%d)\\n", errno);
            runtime·throw("netpoll: GetQueuedCompletionStatusEx failed");
        }
        for(i = 0; i < n; i++) { // 取得した各エントリをループで処理
            op = entries[i].op;
            errno = 0;
            qty = 0;
            // WSAGetOverlappedResult を呼び出し、詳細なI/O結果を取得
            if(runtime·stdcall(runtime·WSAGetOverlappedResult, 5, runtime·netpollfd(op->pd), op, &qty, (uintptr)0, (uintptr)&flags) == 0)
                errno = runtime·getlasterror();
            handlecompletion(&gp, op, errno, qty); // 共通の完了処理関数を呼び出し
        }
    } else { // GetQueuedCompletionStatusEx が利用できない場合 (フォールバック)
        op = nil;
        errno = 0;
        qty = 0;
        // 従来の GetQueuedCompletionStatus を呼び出し
        if(runtime·stdcall(runtime·GetQueuedCompletionStatus, 5, iocphandle, &qty, &key, &op, (uintptr)wait) == 0) {
            errno = runtime·getlasterror();
            if(!block && errno == WAIT_TIMEOUT)
                return nil;
            if(op == nil) {
                runtime·printf("netpoll: GetQueuedCompletionStatus failed (errno=%d)\\n", errno);
                runtime·throw("netpoll: GetQueuedCompletionStatus failed");
            }
        }
        handlecompletion(&gp, op, errno, qty); // 共通の完了処理関数を呼び出し
    }
    // ... (後処理コード) ...
}

// handlecompletion ヘルパー関数の実装
static void
handlecompletion(G **gpp, net_op *op, int32 errno, uint32 qty)
{
    int32 mode;

    if(op == nil)
        runtime·throw("netpoll: GetQueuedCompletionStatus returned op == nil");
    mode = op->mode;
    if(mode != 'r' && mode != 'w') {
        runtime·printf("netpoll: GetQueuedCompletionStatus returned invalid mode=%d\\n", mode);
        runtime·throw("netpoll: GetQueuedCompletionStatus returned invalid mode");
    }
    op->errno = errno;
    op->qty = qty;
    runtime·netpollready(gpp, op->pd, mode); // ゴルーチンを実行可能状態にする
}

src/pkg/runtime/os_windows.c の変更点

このファイルはWindows固有のOSレベルの初期化とユーティリティ関数を含みます。

// GetQueuedCompletionStatusEx 関数ポインタの宣言
void *runtime·GetQueuedCompletionStatusEx;

// runtime·osinit 関数の変更部分
void
runtime·osinit(void)
{
    // ... (既存の初期化コード) ...

    // GetQueuedCompletionStatusEx を動的にロード
    runtime·GetQueuedCompletionStatusEx = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "GetQueuedCompletionStatusEx");
    // ... (既存の初期化コード) ...
}

これらのコード変更により、GoランタイムはWindowsの新しいAPIを活用し、ネットワークI/Oの効率を大幅に向上させています。特に GetQueuedCompletionStatusEx によるバッチ処理と、handlecompletion 関数による共通化された完了処理が、この最適化の核心です。

関連リンク

参考にした情報源リンク