У каждой многозадачной ОС есть что-то, называемое планировщиком процессов. Это компонент ОС, который решает, где и когда запускать каждый процесс. Планировщики обычно довольно упрямы в принимаемых ими решениях, но на них часто могут влиять различные политики и подсказки, предоставленные пользователем. Конфигурация по умолчанию для почти любого планировщика заключается в попытке распространить нагрузку на все доступные процессоры, что часто приводит к миграции процессов с одного процессора на другой. К счастью, любая современная ОС, кроме «самой передовой настольной ОС» (a.k.a. OS X), поддерживает что-то, называемое процессорным сродством. Каждый процесс имеет набор процессоров, на которых ему разрешено выполнять - так называемый набор аффинности процессора этого процесса. Путем настройки непересекающихся наборов аффинности на различные процессы они могут выполняться одновременно без кражи процессорного времени друг от друга. Явное совместимость с процессором поддерживается в Linux, FreeBSD (с планировщиком ULE), Windows NT (это также включает все настольные версии с Windows XP) и, возможно, другие ОС (но не OS X). Каждая ОС затем предоставляет набор вызовов ядра для манипулирования связью, а также инструмент для этого, не создавая специальной программы. В Linux это делается с использованием системного вызова sched_setaffinity(2)
и инструмента командной строки taskset
. Affinity можно также контролировать, создав экземпляр cpuset
. В Windows используется SetProcessAffinityMask()
и/или SetThreadAffinityMask()
, а в диспетчере задач из контекстного меню для заданного процесса можно установить аффинности. Также можно указать желаемую маску сродства в качестве параметра команды оболочки START
при запуске новых процессов.
Что это все связано с OpenMP, так это то, что большинство запусков OpenMP для перечисленных ОС поддерживают в той или иной форме, чтобы указать желаемое отношение к процессору для каждого потока OpenMP. Простейшим элементом управления является переменная среды OMP_PROC_BIND
. Это простой переключатель - при установке на TRUE
он инструктирует среду выполнения OpenMP «привязать» к каждому потоку, т. Е. Дать ему набор аффинити, включающий только один процессор. Фактическое размещение потоков в ЦП зависит от реализации, и каждая реализация обеспечивает свой собственный способ управления им. Например, среда выполнения GNU OpenMP (libgomp
) считывает переменную среды GOMP_CPU_AFFINITY
, а среда Intel OpenMP (с открытым исходным кодом с недавнего времени) читает переменную среды KMP_AFFINITY
.
Обоснование заключается в том, что вы можете ограничить сродство своей программы таким образом, чтобы использовать только подмножество всех доступных процессоров. Затем оставшиеся процессы получат доступ к остальным процессорам, но это гарантируется только в том случае, если вы вручную установили их близость (что возможно только при наличии доступа root/Administrator, поскольку в противном случае вы можете изменить сродство только процессов, которые у вас есть).
Следует отметить, что часто (но не всегда) нет смысла запускать больше потоков, чем количество процессоров в наборе аффинити. Например, если вы ограничите свою программу на 60 процессоров, то использование 64 потоков приведет к тому, что некоторые процессоры будут переписываться и в режиме разговора между потоками. Это заставит некоторые потоки работать медленнее, чем другие. Планирование по умолчанию для большинства рабочих циклов OpenMP равно schedule(static)
, и поэтому общее время выполнения параллельной области определяется временем выполнения самого медленного потока. Если один поток раздается с другим, то оба потока будут выполняться медленнее, чем те потоки, которые не занимаются таймером, и вся параллельная область будет задерживаться. Мало того, что это уменьшает параллельную производительность, но также приводит к растратам, поскольку более быстрые потоки просто просто ничего не предпринимают (возможно, заняты петлями на неявном барьере в конце параллельной области). Решение состоит в том, чтобы использовать динамическое планирование, т.е .:
#pragma omp parallel for schedule(dynamic,chunk_size)
for (int out = 1; out <= matrix.rows; out++)
{
...
}
, где chunk_size
является размером итерационного фрагмента, который получает каждый поток. Все итерационное пространство делится на куски chunk_size
итераций и передается рабочим потокам по принципу «первым пришел-первым-обслужен». Размер блока является важным параметром. Если он слишком низок (по умолчанию - 1), тогда из среды выполнения OpenMP может возникнуть огромная нагрузка, управляющая динамическим планированием. Если он слишком высок, то может быть недостаточно работы для каждого потока. Нет смысла иметь размер куска больше, чем maxtrix.rows/#threads
.
Динамическое планирование позволяет вашей программе адаптироваться к доступным ресурсам ЦП, даже если они не являются однородными, например. если есть другие процессы, выполняющие и разделяющие время с текущим. Но он поставляется с уловкой: большая система, такая как ваш 64-ядерный, обычно представляет собой системы ccNUMA (кэш-когерентные неравномерные системы доступа к памяти), что означает, что каждый процессор имеет свой собственный блок памяти и доступ к блоку (-ам) памяти другие CPU (ы) являются дорогостоящими (например, занимает больше времени и/или обеспечивает меньшую пропускную способность). Динамическое планирование имеет тенденцию разрушать местоположение данных, поскольку нельзя быть уверенным, что блок памяти, который находится на одном NUMA, не будет использоваться потоком, запущенным на другом узле NUMA. Это особенно важно, когда наборы данных большие и не вписываются в кэши CPU. Поэтому YMMV.
Вам не нужно вызывать 'omp_set_num_threads()' для управления количеством потоков. Просто установите переменную среды OMP_NUM_THREADS' в нужное максимальное количество потоков. –