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 函数 GetNumaAvailableMemoryNodeEx、GetNumaNodeProcessorMaskEx、GetNumaProcessorNodeEx 和 GetNumaProximityNodeEx 不同于其未扩展的对应项,即节点号是 USHORT 值而不是 UCHAR,以适应系统上具有 64 个以上的逻辑处理器的节点数。 此外,扩展函数指定或检索的处理器包括处理器组;由未扩展函数指定或检索的处理器是相对于组的。 有关详细信息,请参阅单个函数参考主题。
组感知应用程序可以使用相应的扩展 NUMA 函数,以类似于本主题前面所述的类似方式将其所有线程分配给特定节点。 应用程序使用 GetLogicalProcessorInformationEx 获取系统上所有处理器的列表。 请注意,应用程序无法设置进程关联掩码,除非将进程分配给单个组,并且预期节点位于该组中。 通常,应用程序必须调用 SetThreadGroupAffinity ,以将其线程限制为预期节点。
从 Windows 10 内部版本 20348 开始的行为
注意
从 Windows 10 Build 20348 开始,此函数和其他 NUMA 函数的行为已修改,以更好地支持包含更多 64 个处理器的节点的系统。
创建“假”节点以容纳组和节点之间的 1:1 映射,导致报告 NUMA 节点意外数量的混乱行为,因此,从 Windows 10 内部版本 20348 开始,OS 已更改为允许多个组与节点关联,因此现在可以报告系统的真实 NUMA 拓扑。
在 OS 的这些更改中,许多 NUMA API 已更改,以支持报告多个组,这些组现在可以与单个 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 节点的生成表的潜在问题,该 API 不再提供系统中所有处理器的完整覆盖,这可能会导致表不完整。 这种不完整的含义取决于表的内容。 如果表只存储相应的节点号,则这可能是一个性能问题,发现处理器作为节点 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 或 SetProcessDefaultCpuSetSets 设置的进程默认集中 CPU 集的列表。 |
GetThreadSelectedCpuSetMasks | Windows 10内部版本 20348 中的新增功能。 设置指定线程的所选 CPU 集分配。 如果设置了进程默认分配,则此分配将替代进程默认分配。 |
MapViewOfFileExNuma | 地图文件映射到调用进程的地址空间的视图,并指定物理内存的 NUMA 节点。 |
SetProcessDefaultCpuSetMasks | Windows 10内部版本 20348 中的新增功能。 设置指定进程中线程的默认 CPU 集分配。 |
SetThreadSelectedCpuSetMasks | Windows 10内部版本 20348 中的新增功能。 设置指定线程的所选 CPU 集分配。 如果设置了进程默认分配,则此分配将替代进程默认分配。 |
VirtualAllocExNuma | 保留或提交指定进程的虚拟地址空间中的内存区域,并为物理内存指定 NUMA 节点。 |
QueryWorkingSetEx 函数可用于检索分配页面的 NUMA 节点。 有关示例,请参阅 从 NUMA 节点分配内存。
相关主题