Оптимизация приложений под архитектуру NUMA

Optimizing Applications for NUMA [Eng., PDF 225KB]

Аннотация

NUMA (Non-Uniform Memory Access) – это архитектура совместного доступа к памяти в многопроцессорных системах, в которой время доступа к участку памяти определяется его расположением относительно процессора. Как и в случае с большинством других свойств процессорных систем, невнимание к особенностям архитектуры может привести к ухудшению работы памяти. К счастью, существует возможность нивелировать проблемы в работе, связанные с характерными особенностями NUMA-архитектур и даже использовать некоторые её преимущества для улучшения работы приложений. Это касается привязки потоков к процессорам, распределения памяти с использованием неявных методов, а также применения системных API для привязки ресурсов и перемещения страниц между узлами вычислительной системы.

Введение

Возможно, лучший способ понять NUMA – это сравнить ее с похожей архитектурой UMA (Uniform Memory Access). В UMA-системах все процессоры имеют доступ к совместно используемой памяти через общую шину (или другой вид соединения), как показано на рисунке:

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

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

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

Современные многопроцессорные системы совмещают в себе вышеописанные основные системы, как показано на следующем рисунке:

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

Рекомендации

Два ключевых момента в управлении работой в рамках архитектуры совместного использования памяти NUMA – это привязка потоков к процессорам и правильное размещение данных в памяти.

Привязка потоков к процессорам

Современные сложные операционные системы распределяют потоки между ядрами процессора с помощью планировщика. Планировщик учитывает состояние системы и различные плановые задания (например, «равновесное распределение между процессорами» или «создание потоков на нескольких процессорах и перевод остальных процессоров в режим сна»), затем в соответствии с этим распределяет потоки между физическими ядрами. Определенный поток обрабатывается на определенном ядре в течение некоторого времени, затем выгружается с ядра и ожидает, в то время как другие потоки начинают обрабатываться. Если освобождается другое ядро, планировщик может переместить поток, чтобы обеспечить своевременное выполнение и реализацию плановых задач.

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

Ввод привязки потоков к процессорам. Привязка потоков к процессорам основана на долговременном закреплении точки исполнения протока/процесса на определенных ресурсах системы, несмотря на доступность других устройств. Посредством использования системных API и изменения некоторых структур ОС (например, маски соответствия – affinity mask), определенное ядро или система ядер могут быть ассоциированы с определенным потоком. Затем планировщик отмечает эту привязку в плане распределения на период времени существования потока. Например, поток может быть настроен на работу только на ядрах с 0 по 3, которые принадлежат узлу 0 центрального четырехядерного процессора. Планировщик при этом будет выбирать среди ядер с 0 по 3, даже не предполагая перемещения потока на другой узел.

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

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

Размещение данных с помощью неявных методов

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

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

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

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

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

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

Размещение данных с помощью прямых директив

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

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

Недостатком этой схемы, конечно же, является необходимость для программиста приложения заниматься распределением памяти и размещением данных. Неправильное размещение данных может привести к тому, что работа будет идти значительно хуже, чем если бы она осуществлялась в соответствии со стандартными алгоритмами. Управление памятью в явном виде также предполагает осуществление мелкомодульного контроля привязки задачи к процессору в ходе работы приложения.

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

Руководство по применению

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

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

Дополнительные ресурсы

Parallel Programming Community

Drepper, Ulrich. “What Every Programmer Should Know About Memory”. November 2007.

Intel® 64 and IA-32 Architectures Optimization Reference Manual. See Section 8.8 on “Affinities and Managing Shared Platform Resources”. March 2009.

Lameter, Christoph. “Local and Remote Memory: Memory in a Linux/NUMA System”. June 2006.

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