Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Тематический каталог -> Аппаратное обеспечение

Что каждый программист должен знать о памяти. Часть 6: На что еще способны программисты

Оригинал: Memory, part 6: More things programmers can do
Автор: Ulrich Drepper
Дата публикации: October 31, 2007
Перевод: М.Ульянов
Дата перевода: февраль 2010 г.

6.5 Программирование NUMA

Итак, все, что было сказано об оптимизации работы с кэшем, применимо и к программированию NUMA. Но дальше начинаются отличия. В дело вступает такой факт, как разные затраты при доступе к разным областям адресного пространства. С однородным доступом к памяти мы можем оптимизацией добиться уменьшения ошибок отсутствия страниц (см. раздел 7.5), и на этом все. Все создаваемые страницы памяти одинаковы.

Совсем иначе с NUMA. Затраты на доступ зависят от адресуемой страницы, и это еще больше увеличивает важность оптимизации расположения страниц памяти. Использования архитектуры неоднородного доступа к памяти (именно так расшифровывается NUMA) не избежать на большинстве многопроцессорных машин, поскольку и Intel с CSI (на x86, x86-64 и IA-64), и AMD (на Opteron) используют именно ее. Правда, с увеличением числа ядер на отдельно взятом процессоре количество используемых многопроцессорных машин резко уменьшится (исключение составят дата-центры и ведомства с крайне высокими требованиями к ЦПУ). Для абсолютного большинства домашних машин будет достаточно одного процессора и, соответственно, не будет проблем, связанных с NUMA. Но это: а) не означает, что программисты могут игнорировать архитектуру NUMA и б) не означает исчезновения характерных для этой архитектуры проблем.

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

  • ранние двухъядерные процессоры не имели общего кэша L2.
  • процессоры Core 2 QX 6700 и QX 6800 от Intel, к примеру, имеют четыре ядра и два раздельных кэша L2.
  • как недавно упоминалось, рост числа ядер на одном чипе и стремление к объединению кэшей в итоге приводят к увеличению количества уровней кэша.

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

В разделе 5.3 мы увидели, что ядро Linux может дать нам много полезной - и нужной - информации для программирования NUMA. Однако, собрать эту информацию не так уж легко. Текущая версия Linux-библиотеки NUMA абсолютно непригодна для такой цели. На данный момент автор работает над новой, гораздо более удобной версией.

Существующая библиотека NUMA, libnuma, являющаяся частью пакета numactl, не дает доступа к информации об архитектуре системы. Все, что она делает - собирает в одной упаковке доступные системные вызовы и предоставляет удобные интерфейсы для часто применяемых операций. Итак, на сегодняшний день в Linux доступны следующие системные вызовы:

mbind

Выборочная привязка указанных страниц памяти к узлам.

set_mempolicy

Установить политику выделения памяти по умолчанию.

get_mempolicy

Запросить информацию о политике выделения памяти по умолчанию.

migrate_pages

Переместить все страницы процесса с данного множества узлов на другое множество.

move_pages

Переместить выбранные страницы на указанный узел либо запросить адреса текущих узлов (т.е. на которых на данный момент находятся выбранные страницы).

Эти интерфейсы объявлены в заголовочном файле <numaif.h>, поставляемом с библиотекой libnuma. Но прежде чем углубиться в детали, необходимо понять саму концепцию политик выделения памяти.

6.5.1 Политика выделения памяти

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

Ядром Linux поддерживаются следующие политики:

MPOL_BIND

Память выделяется только из указанного множества узлов. Если это невозможно, память не выделяется.

MPOL_PREFERRED

Память выделяется предпочтительно из указанного множества узлов. Если это невозможно, используется память других узлов.

MPOL_INTERLEAVE

Память выделяется в равной степени поочередно из определенных узлов. Узлы определяются либо смещением в области виртуальной памяти (для политик VMA), либо независимым счетчиком в случае политик задач.

MPOL_DEFAULT

Использовать способ выделения, применяемый по умолчанию для данной области.

Может показаться, что политики в этом списке определяются рекурсивно. Это верно только наполовину. На деле, политики выделения памяти образуют иерархическую структуру (см. рисунок 6.15).

Рисунок 6.15: Иерархическая структура политик выделения памяти

Если адрес принадлежит виртуальной памяти, то применяется соответствующая политика VMA. Для сегментов памяти общего доступа также применяется своя особая политика. Когда такие "специальные" политики для данного адреса отсутствуют, используется политика задач. Ну а если и последняя не назначена, то остается системная политика по умолчанию.

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

6.5.2 Установка политик

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

#include <numaif.h>

long set_mempolicy(int mode, 
                   unsigned long *nodemask, 
		   unsigned long maxnode);

Параметр mode может принимать одно из значений констант, представленных в предыдущем разделе. nodemask определяет необходимые узлы памяти, а maxnode - число этих узлов (т.е. битов) в nodemask. При использовании MPOL_DEFAULT параметр nodemask игнорируется. Если при использовании MPOL_PREFERRED в качестве nodemask передается пустой указатель, то выбирается локальный узел. Иначе MPOL_PREFERRED использует наименьший номер узла в соответствующем битовом множестве nodemask.

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

6.5.3 Свопинг и политики выделения памяти

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

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

Из вышесказанного следует, что любая информация о привязке узлов, полученная на пользовательском уровне, может оставаться верной лишь в течение короткого промежутка времени. Такая информация - скорее подсказка, чем абсолютная истина. Когда нужны достоверные данные, следует использовать интерфейс get_mempolicy (см. раздел 6.5.5)

Замечания и предложения по переводу принимаются по адресу michael@right-net.ru. М.Ульянов.


Назад Оглавление Вперед