Архитектура NUMA

Традиционная модель для многопроцессорной архитектуры — симметричная многопроцессорная (SMP). В этой модели каждый процессор имеет равный доступ к памяти и вводу-выводу. По мере добавления дополнительных процессоров шина процессора становится ограничением производительности системы.

Разработчики систем используют неоднородный доступ к памяти (NUMA) для увеличения скорости процессора без увеличения нагрузки на шину процессора. Архитектура неоднородна, так как каждый процессор находится близко к некоторым частям памяти и дальше от других частей памяти. Процессор быстро получает доступ к памяти, к которому он близок, в то время как получение доступа к памяти, которая находится дальше, может занять больше времени.

В системе NUMA ЦП расположены в небольших системах, называемых узлами. Каждый узел имеет собственные процессоры и память, а также подключен к более крупной системе через шину, согласованную с кэшем.

Система пытается повысить производительность, планируя потоки на процессорах, которые находятся в том же узле, что и используемая память. Он пытается удовлетворить запросы на выделение памяти из узла, но при необходимости выделяет память от других узлов. Он также предоставляет API, чтобы сделать топологию системы доступной для приложений. Вы можете повысить производительность приложений с помощью функций NUMA для оптимизации планирования и использования памяти.

Прежде всего, необходимо определить расположение узлов в системе. Чтобы получить самый большой номер узла в системе, используйте функцию GetNumaHighestNodeNumber . Обратите внимание, что это число не гарантируется равным общему количеству узлов в системе. Кроме того, узлы с последовательными номерами не обязательно будут близки друг к другу. Чтобы получить список процессоров в системе, используйте функцию GetProcessAffinityMask . Вы можете определить узел для каждого процессора в списке с помощью функции GetNumaProcessorNode . Кроме того, чтобы получить список всех процессоров в узле, используйте функцию GetNumaNodeProcessorMask .

Определив, какие процессоры относятся к тем или иным узлам, можно оптимизировать производительность приложения. Чтобы убедиться, что все потоки для процесса выполняются на одном узле, используйте функцию SetProcessAffinityMask с маской сходства процессов, которая указывает процессоры на одном узле. Это повышает эффективность приложений, потоки которых должны получать доступ к той же памяти. Кроме того, чтобы ограничить количество потоков на каждом узле, используйте функцию SetThreadAffinityMask .

Приложениям с большим объемом памяти потребуется оптимизировать использование памяти. Чтобы получить объем свободной памяти, доступный для узла, используйте функцию GetNumaAvailableMemoryNode . Функция VirtualAllocExNuma позволяет приложению указать предпочтительный узел для выделения памяти. VirtualAllocExNuma не выделяет физические страницы, поэтому он будет успешно выполнен независимо от того, доступны ли страницы на этом узле или в другом месте в системе. Физические страницы выделяются по требованию. Если на предпочтительном узле не хватает страниц, диспетчер памяти будет использовать страницы из других узлов. Если память выбросывается на страницу, при возврате используется тот же процесс.

Поддержка NUMA в системах с более чем 64 логическими процессорами

В системах с более чем 64 логическими процессорами узлы назначаются группам процессоров в соответствии с емкостью узлов. Емкость узла — это количество процессоров, которые присутствуют при запуске системы вместе с дополнительными логическими процессорами, которые можно добавить во время работы системы.

Windows Server 2008, Windows Vista, Windows Server 2003 и Windows XP: Группы процессоров не поддерживаются.

Каждый узел должен быть полностью содержаться в группе. Если емкости узлов относительно малы, система назначает несколько узлов одной группе, выбирая физически близкие друг к другу узлы для повышения производительности. Если емкость узла превышает максимальное количество процессоров в группе, система разделяет узел на несколько небольших узлов, каждый из которых достаточно мал, чтобы поместиться в группу.

Идеальный узел NUMA для нового процесса можно запросить с помощью расширенного атрибута PROC_THREAD_ATTRIBUTE_PREFERRED_NODE при создании процесса. Как и процессор с идеальным потоком, идеальный узел является подсказкой планировщику, который назначает новый процесс группе, содержащей запрошенный узел, если это возможно.

Расширенные функции NUMA GetNumaAvailableMemoryNodeEx, GetNumaNodeProcessorMaskEx, GetNumaProcessorNodeEx и GetNumaProximityNodeEx отличаются от своих нерасширенных аналогов тем, что номер узла является значением USHORT , а не UCHAR для размещения потенциально большего числа узлов в системе с более чем 64 логическими процессорами. Кроме того, процессор, указанный или полученный расширенными функциями, включает в себя группу процессоров; Процессор, указанный с помощью или извлекаемый неподдерждаемыми функциями, является относительным к группе. Дополнительные сведения см. в справочных статьях по отдельным функциям.

Приложение, поддерживающее группу, может назначать все свои потоки определенному узлу так же, как описано ранее в этом разделе, используя соответствующие расширенные функции NUMA. Приложение использует GetLogicalProcessorInformationEx для получения списка всех процессоров в системе. Обратите внимание, что приложение не может задать маску сходства процессов, если процесс не назначен одной группе и в ней не находится предполагаемый узел. Обычно приложение должно вызывать SetThreadGroupAffinity , чтобы ограничить потоки предполагаемым узлом.

Поведение, начиная с Windows 10 сборки 20348

Примечание

Начиная с Windows 10 сборки 20348, поведение этой и других функций NUMA было изменено для улучшения поддержки систем с узлами, содержащими более 64 процессоров.

Создание "поддельных" узлов для сопоставления 1:1 между группами и узлами привело к запутанности поведения, когда сообщается непредвиденное количество узлов NUMA, и поэтому, начиная с Windows 10 сборки 20348, ОС изменилась, чтобы разрешить несколько групп быть связанными с узлом, и поэтому теперь можно сообщить об истинной топологии NUMA системы.

В рамках этих изменений в ОС несколько API NUMA были изменены для поддержки отчетов о нескольких группах, которые теперь могут быть связаны с одним узлом NUMA. Обновленные и новые API помечены в таблице в разделе API NUMA ниже.

Так как удаление разделения узлов может повлиять на существующие приложения, доступно значение реестра, позволяющее вернуться к устаревшему поведению разделения узлов. Разделение узлов можно повторно включить, создав значение REG_DWORD с именем SplitLargeNodes со значением 1 под HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\NUMA. Чтобы изменения этого параметра вступили в силу, потребуется перезагрузка.

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

Примечание

Приложения, обновленные для использования новых функций API, которые сообщают об истинной топологии NUMA, будут по-прежнему правильно работать в системах, где разделение больших узлов было повторно включено с помощью этого раздела реестра.

В следующем примере сначала демонстрируются потенциальные проблемы со сборками таблиц, сопоставляющих процессоры с узлами 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

В следующей таблице описан API NUMA.

Функция Описание
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.
GetThreadSelectedCpuSetMasks Новые возможности в сборке Windows 10 20348. Задает выбранное назначение ЦП Наборы для указанного потока. Это назначение переопределяет назначение процесса по умолчанию, если оно задано.
MapViewOfFileExNuma Сопоставляет представление сопоставления файлов с адресным пространством вызывающего процесса и задает узел NUMA для физической памяти.
SetProcessDefaultCpuSetMasks Новые возможности в сборке Windows 10 20348. Задает назначение ЦП по умолчанию для потоков в указанном процессе.
SetThreadSelectedCpuSetMasks Новые возможности в сборке Windows 10 20348. Задает выбранное назначение ЦП Наборы для указанного потока. Это назначение переопределяет назначение процесса по умолчанию, если оно задано.
VirtualAllocExNuma Резервирует или фиксирует область памяти в виртуальном адресном пространстве указанного процесса и задает узел NUMA для физической памяти.

 

Функцию QueryWorkingSetEx можно использовать для получения узла NUMA, на котором выделена страница. Пример см. в разделе Выделение памяти из узла NUMA.

Выделение памяти с узла NUMA

Несколько процессоров

Группы процессоров