NUMA アーキテクチャ

マルチプロセッサ アーキテクチャの従来のモデルは、対称マルチプロセッサ (SMP) です。 このモデルでは、各プロセッサはメモリと I/O に等しいアクセス権を持っています。 プロセッサが追加されると、プロセッサ バスがシステム パフォーマンスの制限になります。

システム デザイナーは、非均一メモリ アクセス (NUMA) を使用して、プロセッサ バスの負荷を増やすことなくプロセッサ速度を向上させます。 各プロセッサがメモリの一部に近く、メモリの他の部分から離れているため、アーキテクチャは一様ではありません。 プロセッサは、近くにあるメモリにすばやくアクセスできますが、遠くにあるメモリへのアクセスに時間がかかる場合があります。

NUMA システムでは、CPU は ノードと呼ばれる小規模なシステムに配置されます。 各ノードには独自のプロセッサとメモリがあり、キャッシュコヒーレント相互接続バスを介して大規模なシステムに接続されます。

システムは、使用されているメモリと同じノード内にあるプロセッサ上のスレッドをスケジュールすることで、パフォーマンスの向上を試みます。 ノード内からメモリ割り当て要求を満たそうとしますが、必要に応じて他のノードからメモリを割り当てます。 また、システムのトポロジをアプリケーションで使用できるようにする API も提供します。 NUMA 関数を使用してスケジュールとメモリ使用量を最適化することで、アプリケーションのパフォーマンスを向上させることができます。

まず、システム内のノードのレイアウトを決定する必要があります。 システム内の最も高い番号のノードを取得するには、 GetNumaHighestNodeNumber 関数を使用します。 この数は、システム内のノードの合計数と一致することは保証されないことに注意してください。 また、連続する数値を持つノードが近接している保証はありません。 システム上のプロセッサの一覧を取得するには、 GetProcessAffinityMask 関数を使用します。 GetNumaProcessorNode 関数を使用して、一覧内の各プロセッサのノードを確認できます。 または、ノード内のすべてのプロセッサの一覧を取得するには、 GetNumaNodeProcessorMask 関数を使用します。

どのプロセッサがどのノードに属するかを決定したら、アプリケーションのパフォーマンスを最適化できます。 プロセスのすべてのスレッドが同じノードで確実に実行されるようにするには、同じノード内のプロセッサを指定するプロセス アフィニティ マスクと共に SetProcessAffinityMask 関数を使用します。 これにより、スレッドが同じメモリにアクセスする必要があるアプリケーションの効率が向上します。 または、各ノードのスレッド数を制限するには、 SetThreadAffinityMask 関数を使用します。

メモリを集中的に消費するアプリケーションでは、メモリ使用量を最適化する必要があります。 ノードで使用できる空きメモリの量を取得するには、 GetNumaAvailableMemoryNode 関数を使用します。 VirtualAllocExNuma 関数を使用すると、アプリケーションでメモリ割り当ての優先ノードを指定できます。 VirtualAllocExNuma は物理ページを割り当てないため、そのノードまたはシステム内の他の場所でページを使用できるかどうかに関係なく成功します。 物理ページはオンデマンドで割り当てられます。 優先ノードがページを使い切った場合、メモリ マネージャーは他のノードのページを使用します。 メモリがページ アウトされている場合は、同じプロセスが取り込まれるときに使用されます。

64 を超える論理プロセッサを搭載したシステムでの NUMA のサポート

論理プロセッサが 64 を超えるシステムでは、ノードの容量に応じてノードが プロセッサ グループ に割り当てられます。 ノードの容量は、システムの実行中に追加できる追加の論理プロセッサと共にシステムが起動したときに存在するプロセッサの数です。

Windows Server 2008、Windows Vista、Windows Server 2003、Windows XP: プロセッサ グループはサポートされていません。

各ノードは、グループ内に完全に含まれている必要があります。 ノードの容量が比較的小さい場合、システムは複数のノードを同じグループに割り当て、パフォーマンスを向上させるために物理的に近いノードを選択します。 ノードの容量がグループ内のプロセッサの最大数を超える場合、システムはノードを複数の小さなノードに分割し、それぞれがグループに収まるほど小さくなります。

プロセスの作成時に、 PROC_THREAD_ATTRIBUTE_PREFERRED_NODE 拡張属性を使用して、新しいプロセスに最適な NUMA ノードを要求できます。 スレッド理想的なプロセッサと同様に、理想的なノードはスケジューラへのヒントであり、可能であれば要求されたノードを含むグループに新しいプロセスを割り当てます。

拡張 NUMA 関数 GetNumaAvailableMemoryNodeExGetNumaNodeProcessorMaskExGetNumaProcessorNodeExおよび GetNumaProximityNodeEx は、ノード番号が UCHAR ではなく USHORT 値であるという点で、64 を超える論理プロセッサを持つシステム上のノードの数が多くなる可能性がある点で、それらの対応する関数とは異なります。 また、拡張関数で指定または取得されたプロセッサには、プロセッサ グループが含まれます。指定されていない関数で指定または取得されたプロセッサは、グループ相対です。 詳細については、個々の関数リファレンス トピックを参照してください。

グループ対応アプリケーションは、対応する拡張 NUMA 関数を使用して、このトピックで前述したのと同様の方法で、すべてのスレッドを特定のノードに割り当てることができます。 アプリケーションでは 、GetLogicalProcessorInformationEx を使用して、システム上のすべてのプロセッサの一覧を取得します。 プロセスが 1 つのグループに割り当てられ、目的のノードがそのグループに配置されていない限り、アプリケーションでプロセス アフィニティ マスクを設定できないことに注意してください。 通常、アプリケーションは SetThreadGroupAffinity を呼び出して、スレッドを目的のノードに制限する必要があります。

Windows 10 ビルド 20348 以降の動作

注意

Windows 10 ビルド 20348 以降では、この関数とその他の NUMA 関数の動作が変更され、64 プロセッサを含むノードを持つシステムのサポートが向上しました。

グループとノード間の 1 対 1 のマッピングに対応するために "偽" ノードを作成すると、予期しない数の NUMA ノードが報告されるという混乱を招く動作が発生しました。そのため、Windows 10ビルド 20348 以降では、OS が変更され、複数のグループをノードに関連付けることができ、システムの真の NUMA トポロジを報告できるようになりました。

OS に対するこれらの変更の一環として、複数のグループのレポートをサポートするように多数の NUMA API が変更され、1 つの NUMA ノードに関連付けることができるようになりました。 更新された API と新しい API には、以下の NUMA API セクションの表にラベルが付けられます。

ノード分割の削除は既存のアプリケーションに影響を与える可能性があるため、レジストリ値を使用して、レガシ ノード分割の動作にオプトバックできます。 ノード分割を再度有効にするには、"SplitLargeNodes" という名前の REG_DWORD 値を作成し、HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMAの下に値 1 を指定します。 この設定の変更を有効にするには再起動が必要です。

reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NUMA" /v SplitLargeNodes /t REG_DWORD /d 1

注意

真の NUMA トポロジを報告する新しい API 機能を使用するように更新されたアプリケーションは、このレジストリ キーを使用して大規模なノード分割が再び可能になったシステムで引き続き正常に動作します。

次の例では、まず、レガシ アフィニティ API を使用してプロセッサを NUMA ノードにマッピングするビルド テーブルに関する潜在的な問題を示します。これにより、システム内のすべてのプロセッサの完全なカバーが提供されなくなり、テーブルが不完全になる可能性があります。 このような不完全性の影響は、テーブルの内容によって異なります。 テーブルに対応するノード番号が格納されているだけの場合、これは、ノード 0 の一部として未発見のプロセッサが残されている場合のパフォーマンスの問題である可能性があります。 ただし、テーブルにノードごとのコンテキスト構造へのポインターが含まれている場合、実行時に NULL 逆参照が発生する可能性があります。

次に、この問題の 2 つの回避策を示すコード例を示します。 1 つ目は、マルチグループ ノード アフィニティ API (ユーザー モードとカーネル モード) に移行することです。 2 つ目は、 KeQueryLogicalProcessorRelationship を使用して、指定されたプロセッサ番号に関連付けられている NUMA ノードに直接クエリを実行することです。


//
// Problematic implementation using KeQueryNodeActiveAffinity.
//

USHORT CurrentNode;
USHORT HighestNodeNumber;
GROUP_AFFINITY NodeAffinity;
ULONG ProcessorIndex;
PROCESSOR_NUMBER ProcessorNumber;

HighestNodeNumber = KeQueryHighestNodeNumber();
for (CurrentNode = 0; CurrentNode <= HighestNodeNumber; CurrentNode += 1) {

    KeQueryNodeActiveAffinity(CurrentNode, &NodeAffinity, NULL);
    while (NodeAffinity.Mask != 0) {

        ProcessorNumber.Group = NodeAffinity.Group;
        BitScanForward(&ProcessorNumber.Number, NodeAffinity.Mask);

        ProcessorIndex = KeGetProcessorIndexFromNumber(&ProcessorNumber);

        ProcessorNodeContexts[ProcessorIndex] = NodeContexts[CurrentNode;]

        NodeAffinity.Mask &= ~((KAFFINITY)1 << ProcessorNumber.Number);
    }
}

//
// Resolution using KeQueryNodeActiveAffinity2.
//

USHORT CurrentIndex;
USHORT CurrentNode;
USHORT CurrentNodeAffinityCount;
USHORT HighestNodeNumber;
ULONG MaximumGroupCount;
PGROUP_AFFINITY NodeAffinityMasks;
ULONG ProcessorIndex;
PROCESSOR_NUMBER ProcessorNumber;
NTSTATUS Status;

MaximumGroupCount = KeQueryMaximumGroupCount();
NodeAffinityMasks = ExAllocatePool2(POOL_FLAG_PAGED,
                                    sizeof(GROUP_AFFINITY) * MaximumGroupCount,
                                    'tseT');

if (NodeAffinityMasks == NULL) {
    return STATUS_NO_MEMORY;
}

HighestNodeNumber = KeQueryHighestNodeNumber();
for (CurrentNode = 0; CurrentNode <= HighestNodeNumber; CurrentNode += 1) {

    Status = KeQueryNodeActiveAffinity2(CurrentNode,
                                        NodeAffinityMasks,
                                        MaximumGroupCount,
                                        &CurrentNodeAffinityCount);
    NT_ASSERT(NT_SUCCESS(Status));

    for (CurrentIndex = 0; CurrentIndex < CurrentNodeAffinityCount; CurrentIndex += 1) {

        CurrentAffinity = &NodeAffinityMasks[CurrentIndex];

        while (CurrentAffinity->Mask != 0) {

            ProcessorNumber.Group = CurrentAffinity.Group;
            BitScanForward(&ProcessorNumber.Number, CurrentAffinity->Mask);

            ProcessorIndex = KeGetProcessorIndexFromNumber(&ProcessorNumber);

            ProcessorNodeContexts[ProcessorIndex] = NodeContexts[CurrentNode];

            CurrentAffinity->Mask &= ~((KAFFINITY)1 << ProcessorNumber.Number);
        }
    }
}

//
// Resolution using KeQueryLogicalProcessorRelationship.
//

ULONG ProcessorCount;
ULONG ProcessorIndex;
SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX ProcessorInformation;
ULONG ProcessorInformationSize;
PROCESSOR_NUMBER ProcessorNumber;
NTSTATUS Status;

ProcessorCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS);

for (ProcessorIndex = 0; ProcessorIndex < ProcessorCount; ProcessorIndex += 1) {

    Status = KeGetProcessorNumberFromIndex(ProcessorIndex, &ProcessorNumber);
    NT_ASSERT(NT_SUCCESS(Status));

    ProcessorInformationSize = sizeof(ProcessorInformation);
    Status = KeQueryLogicalProcessorRelationship(&ProcessorNumber,
                                                    RelationNumaNode,
                                                    &ProcessorInformation,
                                                    &ProcesorInformationSize);
    NT_ASSERT(NT_SUCCESS(Status));

    NodeNumber = ProcessorInformation.NumaNode.NodeNumber;

    ProcessorNodeContexts[ProcessorIndex] = NodeContexts[NodeNumber];
}

NUMA API

次の表では、NUMA API について説明します。

機能 説明
AllocateUserPhysicalPagesNuma 指定したプロセスの アドレス ウィンドウ拡張機能 (AWE) 領域内でマップおよびマップ解除される物理メモリ ページを割り当て、物理メモリの NUMA ノードを指定します。
CreateFileMappingNuma 指定したファイルの名前付きまたは名前のないファイル マッピング オブジェクトを作成または開き、物理メモリの NUMA ノードを指定します。
GetLogicalProcessorInformation Windows 10 ビルド 20348 で更新されました。 論理プロセッサと関連ハードウェアに関する情報を取得します。
GetLogicalProcessorInformationEx Windows 10 ビルド 20348 で更新されました。 論理プロセッサと関連ハードウェアの関係に関する情報を取得します。
GetNumaAvailableMemoryNode 指定したノードで使用可能なメモリの量を取得します。
GetNumaAvailableMemoryNodeEx USHORT 値として指定されたノードで使用可能なメモリの量を取得します。
GetNumaHighestNodeNumber 現在最も大きい数値を持つノードを取得します。
GetNumaNodeProcessorMask Windows 10 ビルド 20348 で更新されました。 指定したノードのプロセッサ マスクを取得します。
GetNumaNodeProcessorMask2 Windows 10 ビルド 20348 の新機能。 指定したノードのマルチグループ プロセッサ マスクを取得します。
GetNumaNodeProcessorMaskEx Windows 10 ビルド 20348 で更新されました。 USHORT 値として指定されたノードのプロセッサ マスクを取得します。
GetNumaProcessorNode 指定したプロセッサのノード番号を取得します。
GetNumaProcessorNodeEx 指定したプロセッサの USHORT 値としてノード番号を取得します。
GetNumaProximityNode 指定した近接識別子のノード番号を取得します。
GetNumaProximityNodeEx 指定した近接識別子の USHORT 値としてノード番号を取得します。
GetProcessDefaultCpuSetMasks Windows 10 ビルド 20348 の新機能。 SetProcessDefaultCpuSetMasks または SetProcessDefaultCpuSets によって設定されたプロセスの既定のセット内の CPU セットの一覧を取得します。
GetThreadSelectedCpuSetMasks Windows 10 ビルド 20348 の新機能。 指定したスレッドに対して選択した CPU セットの割り当てを設定します。 この割り当ては、プロセスの既定の割り当てが設定されている場合にオーバーライドされます。
MapViewOfFileExNuma ファイル マッピングのビューを呼び出し元プロセスのアドレス空間にマップし、物理メモリの NUMA ノードを指定します。
SetProcessDefaultCpuSetMasks Windows 10 ビルド 20348 の新機能。 指定したプロセスのスレッドに対する既定の CPU セットの割り当てを設定します。
SetThreadSelectedCpuSetMasks Windows 10 ビルド 20348 の新機能。 指定したスレッドに対して選択した CPU セットの割り当てを設定します。 この割り当ては、プロセスの既定の割り当てが設定されている場合にオーバーライドされます。
VirtualAllocExNuma 指定したプロセスの仮想アドレス空間内のメモリ領域を予約またはコミットし、物理メモリの NUMA ノードを指定します。

 

QueryWorkingSetEx 関数を使用して、ページが割り当てられている NUMA ノードを取得できます。 例については、「 NUMA ノードからのメモリの割り当て」を参照してください。

NUMA ノードからのメモリの割り当て

複数のプロセッサ

プロセッサ グループ