共用方式為


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底下建立值 1,即可重新啟用節點分割。 此設定的變更需要重新啟動才會生效。

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) 區域中對應和取消對應,並指定實體記憶體的 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 節點設定記憶體

多個處理器

處理器群組