Когда вычисления с ограниченной пропускной способностью памяти выполняются в средах с общей памятью (например, с потоками через OpenMP, Pthreads или TBB), возникает дилемма, как обеспечить правильное распределение памяти по физической памяти, так что каждый поток в основном обращается к памяти в «местная» шина памяти. Хотя интерфейсы не являются переносимыми, большинство операционных систем имеют способы установки схожести потоков (например, pthread_setaffinity_np()
во многих системах POSIX, sched_setaffinity()
в Linux, SetThreadAffinityMask()
в Windows). Существуют также библиотеки, такие как hwloc, для определения иерархии памяти, но, к сожалению, большинство операционных систем пока не предоставляют способы установки политик памяти NUMA. Linux является заметным исключением, с libnumaпозволяя приложению манипулировать политикой памяти и миграцией страниц с гранулярностью страниц (в основном с 2004 года, поэтому широко доступна). Другие операционные системы ожидают, что пользователи будут соблюдать неявную политику «первого прикосновения».
Работа с политикой «первого прикосновения» означает, что вызывающая сторона должна создавать и распространять потоки с той привязкой, которую они планируют использовать позже при первой записи в только что выделенную память. (Очень немногие системы настроены таким образом, что malloc()
фактически находят страницы, просто обещают найти их, когда они действительно повреждены, возможно, разными потоками.) Это подразумевает, что распределение с использованием calloc()
или немедленная инициализация памяти после выделения memset()
вредно, так как это приведет к сбою вся память на шину памяти ядра, на котором выполняется распределительный поток, что приводит к пропускной способности памяти в худшем случае при обращении к памяти из нескольких потоков. То же самое относится к new
оператору C ++, который настаивает на инициализации многих новых распределений (например,std::complex
). Некоторые наблюдения об этой среде:
- Распределение можно сделать «коллективным потоком», но теперь распределение становится смешанным с моделью потоков, что нежелательно для библиотек, которым, возможно, придется взаимодействовать с клиентами, использующими разные модели потоков (возможно, каждая со своими собственными пулами потоков).
- Считается, что RAII является важной частью идиоматического C ++, но, похоже, он активно наносит ущерб производительности памяти в среде NUMA. Размещение
new
может использоваться с памятью, выделенной черезmalloc()
или из подпрограммlibnuma
, но это меняет процесс выделения (который, я считаю, необходим). - РЕДАКТИРОВАТЬ: мое предыдущее утверждение об операторе
new
было неверным, он может поддерживать несколько аргументов, см. Ответ Четана. Я полагаю, что все еще существует проблема получения библиотек или контейнеров STL для использования указанного соответствия. Несколько полей могут быть упакованы, и может быть неудобно гарантировать, например,std::vector
перераспределение с правильным активным менеджером контекста. - Каждый поток может выделить и сбросить свою собственную личную память, но тогда индексация в соседние регионы будет более сложной. (Рассмотрим разреженное матрично-векторное произведение с разделением строки матрицы и векторов; для индексации неизвестной части x требуется более сложная структура данных, когда x не является смежным в виртуальной памяти.)
Какие-либо решения для распределения / инициализации NUMA считаются идиоматическими? Я пропустил другие критические ошибки?
(Я не имею в виду для моего C примеры ++ подразумевает акцент на том языке, однако C ++ язык кодирует некоторые решения об управлении памятью , что язык , как C не происходит , таким образом , существует тенденция к более сопротивления , когда предполагая , что C ++ программисты делают те , все по-другому.)
источник
Этот ответ является ответом на два неправильных представления C ++ в этом вопросе.
Это не прямой ответ на многоядерные проблемы, которые вы упоминаете. Просто отвечаю на комментарии, которые классифицируют программистов на C ++ как фанатиков C ++, чтобы поддерживать репутацию;).
К пункту 1. C ++ «новое» или выделение стека не требует инициализации новых объектов, независимо от того, POD или нет. Конструктор класса по умолчанию, как определено пользователем, несет эту ответственность. Первый код ниже показывает ненужную распечатку, независимо от того, является ли класс POD или нет.
К пункту 2. C ++ позволяет перегружать «new» несколькими аргументами. Второй код ниже показывает такой случай для выделения отдельных объектов. Это должно дать представление и, возможно, быть полезным для вашей ситуации. Оператор new [] также может быть изменен соответствующим образом.
// Код для пункта 1.
Компилятор Intel 11.1 показывает этот вывод (который, конечно, является неинициализированной памятью, отмеченной «а»).
// Код для пункта 2.
источник
std::complex
которых являются явно инициализированы.std::complex
?В сделке. II у нас есть программная инфраструктура для распараллеливания сборки в каждой ячейке на несколько ядер с использованием потоковых строительных блоков (по сути, у вас есть одна задача на ячейку, и вам нужно запланировать эти задачи на доступные процессоры - это не так реализовано но это общая идея). Проблема заключается в том, что для локальной интеграции вам нужно несколько временных (чистых) объектов, и вам нужно предоставить как минимум столько же задач, сколько может выполняться параллельно. Мы видим низкое ускорение, возможно, потому, что когда задача помещается в процессор, она захватывает один из чистых объектов, который обычно находится в кэше другого ядра. У нас было два вопроса:
(i) Это действительно причина? Когда мы запускаем программу в cachegrind, я вижу, что я использую в основном то же количество инструкций, что и при запуске программы в одном потоке, но общее время выполнения, накопленное во всех потоках, намного больше, чем в однопоточном. Это действительно потому, что я постоянно ошибаюсь в кеше?
(ii) Как я могу узнать, где я нахожусь, где находится каждый из «чистых» объектов, и какой «чистый» объект мне нужно взять, чтобы получить доступ к тому, который горячий в кеше моего текущего ядра?
В конечном счете, мы не нашли ответов ни на одно из этих решений, и после нескольких работ решили, что нам не хватает инструментов для исследования и решения этих проблем. Я знаю, как, по крайней мере, в принципе решить проблему (ii) (а именно, использовать локальные объекты потока, предполагая, что потоки остаются закрепленными на ядрах процессора - еще одна гипотеза, которая не является тривиальной для тестирования), но у меня нет инструментов для тестирования проблемы (я).
Таким образом, с нашей точки зрения, работа с NUMA остается нерешенным вопросом.
источник
Помимо hwloc, есть несколько инструментов, которые могут сообщать о среде памяти кластера HPC и которые могут использоваться для настройки различных конфигураций NUMA.
Я бы порекомендовал LIKWID в качестве одного из таких инструментов, поскольку он избегает подхода, основанного на коде, позволяющего вам, например, закрепить процесс на ядре. Такой подход к инструментам для настройки конфигурации памяти конкретной машины поможет обеспечить переносимость кода между кластерами.
Вы можете найти краткую презентацию с изложением ISC'13 « LIKWID - Инструменты облегченной производительности », и авторы опубликовали статью на Arxiv « Лучшие практики для HPM-инжиниринга производительности на современных многоядерных процессорах ». В этой статье описан подход к интерпретации данных со счетчиков оборудования для разработки производительного кода, специфичного для архитектуры вашего компьютера и топологии памяти.
источник