
Интеграция контроллера оперативной памяти в состав микросхемы CPU была одним из самых закономерных и ожидаемых событий. Преимущества очевидны — микросхем на плате меньше, быстродействие выше. Все просто и предсказуемо. Только не для мультипроцессорных систем: ведь теперь каждый процессор располагает своим контроллером памяти.
Итак, при включении компьютера, после завершения процедуры BIOS POST, в результате инициализации картирующей логики, все модули DIMM многопроцессорной платформы, сгруппированы по каналам и размещены в едином и огромном 64-битном физическом адресном пространстве. Каждый из контроллеров памяти получил свой адресный диапазон. После загрузки ОС, механизмы виртуальной памяти и страничной трансляции скрывают от прикладного программного обеспечения фрагментирование физических ресурсов. Казалось бы, система полностью прозрачна, и программисту не нужно задумываться над тем, к какому процессору подключена используемая память.
В чем проблема?
Нетрудно понять, что для каждого из процессоров, доступ к собственной памяти осуществляется быстрее, чем к памяти, обслуживаемой соседним процессором, так как во втором случае информация должна пройти по шинам межпроцессорной связи. Именно поэтому, такая архитектура получила название NUMA или Non-Uniform Memory Access, что в переводе означает система с неоднородным доступом к памяти.
NUMA API
Очевидно, производительность платформы будет максимальной, если для параллельно работающих программных потоков, информация, интенсивно используемая каждым потоком будет размещена в памяти, подключенной к процессору, на котором выполняется данный поток. Для оптимизации программного обеспечения с учетом специфики NUMA, в среде операционных систем Windows предусмотрены специальные функции, заменяющие либо дополняющие классический набор функций управления памятью.
Для выделения памяти предусмотрена функция VirtualAllocExNuma, которая одним из входных параметров принимает номер узла (фактически процессорного сокета или физического процессора), на котором следует выделить память, поскольку, этот узел будет интенсивно использовать выделяемый диапазон.
При выполнении каждого программного потока может быть вызвана функция SetThreadAffinityMask, задающая 64-битный вектор в котором каждому из 64 возможных логических процессоров соответствует 0 (если потоку не рекомендуется использовать этот процессор) или 1 (если потоку рекомендуется использовать этот процессор). Векторы должны быть сформированы таким образом, чтобы каждый из потоков использовал логические процессоры, входящие только в тот узел, с которым ассоциирован диапазон памяти, используемый этим потоком, ранее выделенный функцией VirtualAllocExNuma.
Для платформ, содержащих более 64 логических процессоров, требуется специальная поддержка. Такие системы разделяются на группы (Processor Groups), при этом в каждой группе может быть не более 64 процессоров. Ранее написанное программное обеспечение, не поддерживающее процессорные группы, сможет использовать только подмножество процессоров, принадлежащих одной группе.
При этом важно понимать, что указания, которые мы даем операционной системе используя NUMA API, носят не обязательный, а рекомендательный характер, поскольку игнорирование этих указаний операционной системой приведет не к сбою выполнения программы, а только некоторому замедлению. Например, если мы запрашиваем выделение памяти, ассоциированной с узлом 0, а вся память узла 0 занята, то операционной системой будет выделена память, не ассоциированная с узлом 0. При этом несколько упадет скорость, но программа сохранит работоспособность.
ACPI System Resource Affinity Table
Рассмотренные выше функции NUMA API обеспечивают взаимодействие операционной системы и NUMA-оптимизированного приложения. Опустимся на ступеньку ниже в иерархии программных интерфейсов и рассмотрим механизм передачи топологической информации от Firmware платформы к ОС. Для этого, в рамках интерфейса ACPI (Advanced Configuration and Power Interface) предусмотрена специальная таблица SRAT (System Resource Affinity Table или, по другой версии Static Resource Affinity Table). Платформа представляется в виде набора узлов или доменов (proximity domains).
С аппаратной точки зрения, домен - это совокупность физического процессора (сокета) и подключенной к нему подсистемы памяти. При этом физический процессор, в силу использования многоядерности и технологии Hyper-Threading может содержать группу логических процессоров. В свою очередь, каждый контроллер оперативной памяти предоставляет диапазон или группу диапазонов адресного пространства, обеспечивающих доступ к памяти.
С программной точки зрения, в каждый домен входит группа логических процессоров и группа диапазонов адресного пространства.

Рис.1. Заголовок таблицы System Resources Affinity Table (SRAT)


Пример
Рассмотрим содержимое ACPI-таблицы System Resource Affinity Table (SRAT) на типовой двухпроцессорной платформе, оснащенной 64 гигабайтами оперативной памяти. Для этого нам потребовалось написать небольшую утилиту dump.ACPI.SRAT, обеспечивающую доступ к структурам Firmware из среды Win64.

Рис.4. Дамп ACPI-таблицы SRAT, полученный на типовой двухпроцессорной платформе с суммарным объемом оперативной памяти 64 гигабайта
Текстовый файл дампа содержит шестнадцатеричный дамп ACPI-таблицы SRAT, а файл расшифровки — результат анализа ее содержимого. Как и ожидалось, декларировано два домена, по количеству процессорных сокетов. Каждый из доменов содержит 16 логических процессоров. В адресации оперативной памяти наблюдается асимметрия, обусловленная стремлением разработчиков разместить memory-mapped I/O ресурсы периферийных устройств ниже границы 4GB, для того, чтобы сделать их доступными 32-битным операционным системам. Вряд ли кому-то придет в голову использовать такие системы на данной платформе, но традиция должна быть соблюдена. 32 гигабайта домена 0 доступны виде двух фрагментов (0-2 GB и 4-34 GB). 32 гигабайта домена 1 доступны в диапазоне 34-66 GB. «Дырка» в диапазоне 2-4GB необходима для доступа 32-битных операционных систем к memory-mapped I/O. Заметим, что 32-битные ОС не могут адресовать память второго процессора (домена 1), но могут его использовать, размещая код и данные в памяти, подключенной к первому процессору (домену 0).
Вместе с тем, 32-битные приложения, работающие в среде 64-битной ОС, лишены указанных неудобств, так как механизм трансляции страниц в режиме PAE (Physical Address Extension) позволяет отобразить любую страницу 32-битного виртуального адресного пространства приложения на любую страницу 64-битного физического адресного пространства платформы. То же самое справедливо для 32-битных гостевых ОС, запущенных с применением 64-битных средств аппаратной виртуализации.
Современные тенденции
Примечательно, что реализация микросхемы центрального процессора в виде мультичиповой конструкции, придает системам с одним процессорным гнездом как позитивные, так и негативные свойства многосокетных NUMA-платформ.

Рис. 5. Исследование производительности оперативной памяти с помощью утилиты NCRB. Односокетная система на основе AMD EPYC 7351
В примере (Рис 5.) 256-битное AVX-чтение выполняется одним потоком, демонстрируя скорость около 21.2 GBPS, близкую к показателям двух каналов одного NUMA-домена. Заметим, в исследуемой платформе четыре NUMA-домена, поэтому общее количество каналов памяти равно 8.
Итак, согласно информации, полученной в результате анализа содержимого таблицы ACPI SRAT (красный прямоугольник на Рис 5), серверная платформа на основе одного процессора AMD EPYC 7351 содержит в общей сложности 32 логических процессора (CPUs=32), распределенных по четырем NUMA-доменам (Domains=4), по числу кристаллов многочиповой конструкции. Количество декларируемых диапазонов памяти (RAMs=6), превышает количество доменов, в силу фрагментации адресного пространства, обусловленной созданием диапазонов совместимости в пределах нижних 4 гигабайт. Такие диапазоны делают возможной 32-битную адресацию memory-mapped ресурсов. Здесь проявилась инерционность разработчиков UEFI firmware, ведь использование 32-битных ОС на платформе с указанным процессором, мягко говоря, не целесообразно.
Топологическая информация, декларируемая NUMA API операционной системы (зеленый прямоугольник на Рис 5), позволяет выделить группы логических процессоров. Переведя в двоичный код содержимое Affinity-масок и выделяя группы битов, установленных в «1», нетрудно определить подмножества логических процессоров, относящихся к каждому из NUMA-доменов.
node 0, mask = 00000000000000FFh, processors [7-0]
node 1, 000000000000FF00h, processors [15-8]
node 2, 0000000000FF0000h, processors [23-16]
node 3, 00000000FF000000h, processors [31-24]
Отметим, что количество доменов, декларируемых операционной системой (NUMA Nodes=8) удвоено в сравнении с информацией, предоставляемой firmware платформы посредством ACPI (Domains=4). Предположительно, наличие неиспользуемых доменов является результатом предпринятой унификации, усложняющей упрощающей отладку и оптимизацию программного обеспечения для односокетных и двухсокетных платформ.
Резюме
Увы, любое равноправие иллюзорно. И в жизни, и в обществе, и в мультипроцессорной системе. Неоднородность доступа к ресурсам в современной платформе требует специальной оптимизации программного обеспечения на уровне Firmware, ОС и приложений.