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 Build 20348 开始,OS 已更改,允许多个组与一个节点相关联,因此现在可以报告系统的真实 NUMA 拓扑。

作为 OS 的这些更改的一部分,许多 NUMA API 已更改,以支持报告多个组,这些组现在可以与单个 NUMA 节点相关联。 更新的 API 和新 API 标记在 NUMA API 部分的表中。

由于删除节点拆分可能会影响现有应用程序,因此可以使用注册表值来允许选择重新加入旧节点拆分行为。 可以通过在HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA下创建名为“SplitLargeNodes”的 REG_DWORD 值来重新启用节点拆分。 对此设置的更改需要重启才会生效。

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 节点分配内存

多个处理器

处理器组