Библиотека сайта rus-linux.net
Что каждый программист должен знать о памяти. Часть 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 в одном файле |