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

UnixForum






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

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

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

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

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

6.5.6 Наборы ЦПУ и узлов памяти

Подгонка кода под определенные среды - SMP и NUMA - путем использования описанных интерфейсов может оказаться задачей непомерно дорогой, а то и невозможной вовсе, если недоступны исходники. К тому же, системный администратор может ограничить ресурсы, доступные для использования пользователям и/или процессам. Для таких ситуаций ядро Linux поддерживает так называемые наборы ЦПУ (CPU sets). Название немного обманчиво, поскольку включает в себя не только процессоры, но и узлы памяти. К тому же, наборы ЦПУ не имеют ничего общего с типом данных cpu_set_t.

На данный момент интерфейсом к наборам ЦПУ служит специальная виртуальная файловая система. Обычно по умолчанию она не смонтирована (по крайней мере, на сегодня это именно так). Исправить этот недочет можно, используя

     mount -t cpuset none /dev/cpuset

Естественно, точка монтирования /dev/cpuset должна уже существовать. Внутри этой директории находится описание корневого (root) набора ЦПУ. Поначалу он включает в себя все процессоры и узлы памяти. Файл cpus там же показывает процессоры, находящиеся на данный момент в наборе, файл mems показывает узлы памяти и файл tasks - процессы.

Для создания нового набора достаточно просто создать новую директорию в любом месте иерархии. Созданный набор унаследует все настройки от родителя, и затем их можно поменять, записывая новые значения в псевдофайлы cpus и mems в новой директории.

Если процесс принадлежит набору ЦПУ, то настройки процессоров и узлов памяти используются в качестве битовых масок привязки к процессорам и политики выделения памяти соответственно. Это означает, что программа не может выбрать никаких других процессоров, кроме прописанных в файле cpus "своего" набора ЦПУ (т.е. в файле tasks которого находится данный процесс). Аналогично для узлов памяти и файла mems.

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

Директории наборов ЦПУ содержат и другие файлы, которые используются для указания различных особенностей конкретного набора - например, для определения поведения при нехватке памяти или для определения исключительного доступа к процессорам и узлам памяти. Заинтересованный читатель может обратиться за подробностями к файлу Documentation/cpusets.txt в дереве исходников ядра.

6.5.7 Явные оптимизации NUMA

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

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

void *local_data(void) {
  static void *data[NNODES];
  int node =
    NUMA_memnode_self_current_idx();
  if (node == -1)
    /* Cannot get node, pick one.  */
    node = 0;
  if (data[node] == NULL)
    data[node] = allocate_data();
  return data[node];
}
void worker(void) {
  void *data = local_data();
  for (...)
    compute using data
}

Функция worker начинает работу с получения указателя на локальную копию данных (для этого происходит вызов local_data). Затем выполняется цикл, использующий полученный указатель. Функция local_data хранит список уже распределенных копий данных. В каждой системе число узлов данных ограничено, поэтому и размер массива указателей на копии данных для каждого узла также ограничен. Взятая из libNUMA функция NUMA_memnode_system_count возвращает как раз это число. Если указатель для текущего узла, определяемый вызовом NUMA_memnode_self_current_idx, не существует, создается новая копия.

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

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

Например, если данная область памяти используется для сбора результатов, можно сначала создать отдельные области в каждом узле памяти, где происходит сбор локальных результатов. А затем, когда эта работа закончена, все такие локальные области объединяются для получения общего результата. Данный прием работает и в случае, если работа на деле никогда не останавливается, но необходимы промежуточные результаты. Требование такого подхода - должно отсутствовать последействие, то есть текущие результаты не должны зависеть от прошлых результатов.

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

6.5.8 Используем всю полосу пропускания

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

Возможно это на практике или нет, зависит от множества факторов. Необходимо быть уверенным, что запись действительно идет мимо кэша, иначе замедление, связанное с удаленным доступом, будет очень даже ощутимо. Другая проблема возникает, если удаленному узлу самому нужен свой канал памяти полностью. Такая вероятность есть, и она должна быть подробно исследована перед применением предложенного подхода. В теории, несомненно, использование всей доступной процессору полосы пропускания может дать положительный эффект. Например, процессор Opteron семейства 10h может быть напрямую соединен с четырьмя другими процессорами, и использование всей этой увеличенной полосы, дополненное соответствующей предвыборкой (особенно prefetchw), просто обязано улучшить производительность. Если, конечно, система уже работает на полную мощность и не имеет других узких мест.

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


Назад Оглавление Вперед
  Вся часть 6 в одном файле