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 _ 线程 _ 属性 _ 首选 _ 节点 扩展属性请求新进程的理想 NUMA 节点。 与线程理想的处理器一样,理想节点是计划程序的提示,它会将新进程分配给包含请求节点的组(如果可能)。

扩展的 NUMA 函数 GetNumaAvailableMemoryNodeExGetNumaNodeProcessorMaskExGetNumaProcessorNodeExGetNumaProximityNodeEx 不同于其 unextended,因为节点号是 USHORT 值而不是 UCHAR,以容纳超过64个逻辑处理器的系统上可能的节点数。 此外,由扩展函数指定或由扩展函数检索的处理器包括处理器组;由 unextended 函数指定或检索的处理器是组相对的。 有关详细信息,请参阅各个函数参考主题。

组感知应用程序可使用相应的扩展 NUMA 函数将其所有线程以类似方式分配给特定节点,如本主题前面所述。 应用程序使用 GetLogicalProcessorInformationEx 获取系统上的所有处理器的列表。 请注意,应用程序无法设置进程关联掩码,除非该进程分配给单个组并且目标节点位于该组中。 通常,应用程序必须调用 SetThreadGroupAffinity ,以将其线程限制到预期节点。

从 Windows 10 生成20348开始的行为

备注

从 Windows 10 生成20348开始,此功能和其他 NUMA 函数的行为已修改,以便更好地支持其节点包含多个64处理器的系统。

创建 "虚假" 节点以容纳组与节点之间的1:1 映射,导致了混乱的行为,其中报告了意外数量的 NUMA 节点,因此从 Windows 10 Build 20348 开始,操作系统已更改为允许多个组与节点相关联,因此现在可以报告系统的真正 NUMA 拓扑。

在对 OS 进行这些更改的过程中,许多 NUMA Api 已更改,支持报告现在可与单个 NUMA 节点相关联的多个组。 下面的 " NUMA API " 部分的表中标记了更新的和新的 api。

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

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

备注

如果使用此注册表项重新启用了大型节点拆分,则更新为使用报告 true 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 内部版本 20348 Windows 10新增功能。 检索由 SetProcessDefaultCpuSetMasks 或 SetProcessDefaultCpuSets 设置的进程默认集中的 CPU 集列表。
GetThreadSelectedCpuSetMasks 内部版本 20348 Windows 10新增功能。 设置指定线程的所选 CPU 集分配。 如果设置了进程默认分配,则此分配将替代该分配。
MapViewOfFileExNuma 地图映射到调用进程的地址空间的文件的视图,并指定物理内存的 NUMA 节点。
SetProcessDefaultCpuSetMasks 内部版本 20348 Windows 10新增功能。 设置指定进程中线程的默认 CPU 集分配。
SetThreadSelectedCpuSetMasks 内部版本 20348 Windows 10新增功能。 设置指定线程的所选 CPU 集分配。 如果设置了进程默认分配,则此分配将替代该分配。
VirtualAllocExNuma 保留或提交指定进程的虚拟地址空间中的内存区域,并指定物理内存的 NUMA 节点。

QueryWorkingSetEx函数可用于检索分配页的 NUMA 节点。 有关示例,请参阅 从 NUMA 节点 分配内存

从 NUMA 节点分配内存

多个处理器

处理器组