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 函式 GetNumaAvailableMemoryNodeExGetNumaNodeProcessorMaskExGetNumaProcessorNodeExGetNumaProximityNodeEx 與其未擴充的對應專案不同,因為節點編號是 USHORT 值,而不是 UCHAR,以容納系統上可能大於 64 個邏輯處理器的節點數目。 此外,擴充函式指定或擷取的處理器包含處理器群組;未擴充函式所指定的處理器是群組相對的。 如需詳細資訊,請參閱個別函式參考主題。

群組感知應用程式可以使用對應的擴充 NUMA 函式,以類似本主題稍早所述的方式,將其所有線程指派給特定節點。 應用程式會使用 GetLogicalProcessorInformationEx 來取得系統上所有處理器的清單。 請注意,除非進程指派給單一群組,且預定的節點位於該群組中,否則應用程式無法設定進程親和性遮罩。 應用程式通常必須呼叫 SetThreadGroupAffinity ,以將其執行緒限制為預定的節點。

從 Windows 10 組建 20348 開始的行為

注意

從 Windows 10 組建 20348 開始,已修改此和其他 NUMA 函式的行為,以更妥善地支援包含更多 64 個處理器節點的系統。

建立「假」節點以容納群組與節點之間的 1:1 對應,導致回報非預期 NUMA 節點數目的混淆行為,因此從 Windows 10組建 20348 開始,OS 已變更為允許多個群組與節點產生關聯,因此現在可以報告系統真正的 NUMA 拓撲。

在 OS 的這些變更過程中,已變更一些 NUMA API 以支援報告多個群組,而此群組現在可以與單一 NUMA 節點相關聯。 更新和新 API 會在下方 NUMA API 區段中的資料表中加上標籤。

因為移除節點分割可能會影響現有的應用程式,所以登錄值可用來允許選擇回到舊版節點分割行為。 您可以建立名為 「SplitLargeNodes」 的REG_DWORD 值,在HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA底下建立名為 「SplitLargeNodes」 的節點分割。 此設定的變更需要重新開機才會生效。

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

注意

更新為使用報告真實 NUMA 拓撲之新 API 功能的應用程式,將會繼續在此登錄機碼重新啟用大型節點分割的系統上正常運作。

下列範例會先示範使用舊版親和性 API 建置資料表將處理器對應至 NUMA 節點的潛在問題,這不會再提供系統中所有處理器的完整涵蓋,這可能會導致資料表不完整。 這類不完整的影響取決於資料表的內容。 如果資料表只是儲存對應的節點編號,則這可能是在節點 0 中留下已發現處理器的效能問題。 不過,如果資料表包含每個節點內容結構的指標,這可能會導致執行時間產生 Null 取值。

接下來,程式碼範例說明問題的兩個因應措施。 第一個是移轉至多群組節點親和性 API, (使用者模式和核心模式) 。 第二個是使用 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 (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 節點配置記憶體

多個處理器

處理器群組