OpenMP создавался как средство для программирования параллельных
вычислений, выполняемых в многопроцессорных системах с общим пространством
памяти (SMP). Его основную часть составляет набор директив, включаемых в
тексты программ, написанных на обычных алгоритмических языках. Первая версия
стандарта для языков C/C++ появилась в октябре 1998 г., а вторая, и пока
последняя - в марте 2002 г., ее полное описание вы найдете на сайте www.openmp.org.
В связи с выпуском микропроцессора Pentium 4 HT, поддерживающего
технологию Hyper Threading, корпорация Intel разработала реализацию стандарта
OpenMP для компиляторов Intel C++. Она позволяет программировать параллельные
вычисления, на двух логических процессорах, входящих в состав Pentium 4 HT и
последних моделей Pentium 4 с новым ядром Prescott.
Список директив, поддерживаемых реализацией Intel, описан в документе "OpenMP*
Standard Option". Судя по этому документу при работе с Intel C++
можно использовать стандартный набор директив OpenMP для C/C++ и почти все
параметры. Поэтому при подготовке данной публикации я использовал описание,
приведенное в стандарте, поскольку оно является более полным.
1.
Директивы
В преамбуле к описанию стандарта сказано, что для использования директив и
всех возможностей OpenMP для C/C++ в командной строке компилятора должен быть
указан специальный ключ. В версии для Intel C++ он называется /Qopenmp.
Директивы OpenMP для C/C++ имеют следующий формат:
#pragma omp <имя директивы> [список параметров]
Как известно, в семействе языков C директива #pragma предназначена
для управления специфическими возможностями компилятора. Ключевое слово omp
используется для того, чтобы исключить случайные совпадения имен директив
OpenMP с другими именами.
Директивы parallel, for, section и single имеют параметры,
которые указываются в той же строке после имени директивы. Если параметры не
указаны, то используются их значения принятые по умолчанию.
Объектом действия большинства директив является один оператор
или блок, перед которым расположена директива в исходном тексте
программы. В описании стандарта OpenMP такие операторы или блоки называются
ассоциированными с директивой.
Параллельный регион. Фрагмент программы, который делится на
параллельно выполняемые нити, называется параллельным регионом. В стандарте
OpenMP нить определена как простая программная компонента,
имеющая частные (локальные) переменные, доступ к общим (внешним) переменным и
предназначенная для выполнения одним процессором. В зависимости от
установленного способа планирования это может быть конкретный или первый
свободный процессор.
Параллельно выполняемые нити формируют директивы for и sections.
Первая из них создает нити, содержащие копии ассоциированного оператора
цикла, каждая из которых выполняет некоторую часть от общего количества
итераций. Вторая формирует нити из отдельных блоков, расположенных в исходной
программе последовательно друг за другом. Перед каждым преобразуемым в нить
блоком указывается вспомогательная директива section.
В сфере действия директив for и sections нельзя использовать
все остальные директивы OpenMP, что ограничивает возможности управления
вычислительным процессом в параллельном регионе. Для снятия этих ограничений
описание региона должно начинаться с директивы parallel. Формально
сферой ее действия является блок, но он может содержать вложенные блоки и
директивы, как формирующие структуру региона, так и управляющие процессом
вычислений. Кроме того, директива parallel имеет параметры, которые
позволяют по усмотрению программиста выбирать количество нитей, выполняемых в
параллельном регионе.
Состав директив и их краткая характеристика приведены в следующей таблице:
Директива
|
Описание
|
parallel [параметры]
|
Директива имеет декларативный характер и не управляет действиями,
выполняемыми в параллельном регионе. Она нужна, например, в тех случаях,
когда для распараллеливания региона используется несколько директив
формирующих нити или выполняющих другие действия.
Параметры: private,
firstprivate, shared, default, reduction, copyin, if, num_threads.
|
for [параметры]
|
Формирует нити, содержащие копии ассоциированного с директивой цикла
типа for. Каждая копия будет выполнять свою часть от общего числа итераций,
описанных в исходном операторе цикла.
Параметры: private,
firstprivate, lastprivate, reduction, ordered, nowait, schedule.
|
sections [параметры]
|
Формирует параллельный регион из блоков, расположенных в исходном тексте
программы последовательно друг за другом. Перед каждым преобразуемым блоком
указывается директива section.
Параметры: private,
firstprivate, lastprivate, reduction, nowait.
|
section
|
Вспомогательная директива, используется только в области действия
директивы sections для формирования нитей из ассоциированных блоков.
|
single [параметры]
|
Указывает на то, что в регионе должна выполняться только одна нить,
содержащая ассоциированный с директивой блок. Такая нить может, например,
изменять значения частных переменных, используемых другими нитями региона.
Параметры: private,
firstprivate, copyprivate, nowait.
|
parallel for
[параметры]
|
Сокращенная форма записи для создания параллельного региона, содержащего
единственную директиву for. Сочетание двух директив увеличивает
количество доступных параметров.
Параметры: private,
firstprivate, lastprivate, shared, default, reduction, ordered, schedule,
copyin, if, num_threads.
|
parallel sections
[параметры]
|
Сокращенная форма записи для создания параллельного региона, содержащего
единственную директиву sections. Сочетание двух директив увеличивает
количество доступных параметров.
Параметры: private,
firstprivate, lastprivate, shared, default, reduction, copyin, if,
num_threads.
|
master
|
Ассоциированный с директивой блок преобразуется в основную нить, с
которой начинается выполнение задачи. Выполнение основной нити продолжается
до тех пор, пока не встретится первая распараллеливаемая конструкция.
|
critical [имя секции]
|
Критические секции нужны для разграничения доступа к общему ресурсу,
например, к памяти. Все нити параллельного региона ждут завершения
выполнения критической секции. Если есть несколько критических секций, то
им надо присвоить уникальные имена.
|
barrier
|
Указывает точку, в которой организуется ожидание окончания исполнения
всех нитей параллельного региона. По умолчанию (если не указан параметр nowait)
директивы for, sections и single устанавливают барьер в
нужной точке региона.
|
atomic
|
Директива действует только на один оператор присваивания. При каждом его
выполнении новое значение переменной, указанной в левой части принудительно
сохраняется в памяти. Это позволяет исключить возможные ошибки при работе с
одной переменной в нескольких нитях параллельного региона.
|
flush
[список переменных]
|
Только указанные в списке, или по умолчанию все общие
переменные подвергаются операции "выравнивание" (flush). При этом
из кеш в основную память переписываются переменные, значения которых были
изменены. Это же касается и переменных находящихся в регистрах процессоров.
|
threadprivate
(список переменных)
|
Объявляет частными в нитях параллельного региона переменные, описанные
во внешнем блоке. Директива указывается во внешнем блоке сразу после
описания соответствующих переменных и не влияет на работу с ними вне
параллельного региона. См. copyin.
|
ordered
|
Используется в сфере действия директивы for для выделения блока,
в котором повторы цикла будут происходить в естественном порядке (как при
обычных последовательных вычислениях). У директивы for должен быть
указан одноименный ключ ordered.
|
2.
Параметры директив
Параметры директив (directive clauses) можно условно разделить на
две категории. Одни из них влияют на ход вычислительного процесса и позволяют
учесть особенности распараллеливаемой задачи и многопроцессорной системы, в
которой она будет выполняться. Например, с их помощью можно разрешить
создание параллельного региона только при выполнении заданного условия,
назначить количество нитей в регионе или количество итераций, выполняемых в
нитях.
Другие параметры, их примерно половина, определяют видимость переменных в
параллельном регионе и специальные действия, которые должны выполняться над
ними в начале и в конце сферы действия директивы. В таких случаях после имени
параметра в круглых скобках указывается список переменных, на которые
распространяется действие параметра. Он может содержать только те имена,
которые описаны в исходном тексте стандартными средствами C/C++ и
используются в ассоциированном с директивой операторе или блоке.
Работа с переменными. По умолчанию действует простое правило
определения видимости переменных в параллельном регионе. Те из них, которые в
исходном тексте программы описаны вне распараллеливаемого оператора или
блока, объявляются общими, а описанные внутри оператора или блока - частными.
Общие (shared) переменные доступны всем нитям, а частные (private)
создаются для каждой нити только на время ее выполнения. Пример общей
переменной - имя массива, элементы которого обрабатываются в теле цикла, а
переменная, управляющая повторами цикла, является частной, иначе будет
невозможно распараллеливание цикла.
Принятое по умолчанию деление переменных на общие и частные применимо не
во всех случаях и тогда приходится явно указывать способ доступа или
особенности использования конкретных переменных. Рассмотрим два случая,
которые возможны при распараллеливании оператора цикла.
Предположим, что результаты вычислений, выполняемых в теле цикла,
накапливаются в переменной sum. По умолчанию она объявляется общей и
доступна всем нитям. Конечный результат окажется корректным, но в процессе
выполнения задания сразу несколько нитей будут обращаться к переменной sum
для чтения или записи ее содержимого. Это неизбежно замедлит процесс
выполнения задания. Просто объявить переменную sum частной нельзя,
поскольку будут потеряны результаты вычислений накопленные в нитях.
Специально для такого случая предназначен параметр reduction. Если в
его списке указать переменную sum, то в нитях она будет использоваться
как частная, а при выходе из каждой нити накопленный результат будет добавлен
к общей переменной sum.
Другой пример. Предположим, что в теле цикла для хранения промежуточных
результатов вычислений используется переменная temp. Если в исходном
тексте ее описание расположено вне оператора цикла, то при распараллеливании
по умолчанию она будет объявлена общей переменной. В таком случае при
выполнении задания вполне вероятны случаи, когда одна из нитей успеет
изменить значение temp, прежде чем другая использует сформированное в
ней значение. Это неизбежно приведет к ошибкам в результатах вычислений.
Поэтому все переменные, используемые для хранения промежуточных результатов,
следует объявлять частными, либо описывать их непосредственно в теле
оператора цикла.
Параметры и их краткое описание приведены в следующей таблице:
Параметр
|
Описание
|
private (список)
|
Для каждой нити на время ее выполнения создаются копии перечисленных в
списке переменных, доступные только в пределах конкретной нити (на
конкретном процессоре). Исходные значения копий не определены.
Применяется с директивами parallel, for, sections, single.
|
firstprivate (список)
|
Отличается от параметра private тем, что создаваемым копиям
присваивается значения, которые имели перечисленные в списке переменные
перед входом в параллельный регион.
Применяется с директивами parallel, for, sections, single.
|
lastprivate (список)
|
Отличается от параметра private тем, что после выхода из цикла или из
последней секции конечные значения перечисленных в списке переменных будут
доступны для использования.
Применяется с директивами for, sections.
|
shared (список)
|
Описывает те переменные исходного блока, которые будут доступны всем
нитям параллельного региона. Другими словами, нити будут работать не с
копиями, а с оригиналами переменных.
Применяется с директивой parallel.
|
default (shared | none)
|
Данный параметр действует на все перемнные, используемые в параллельном
регионе. Default (shared) делает их общими, а default (none)
недоступными. В последнем случае необходимо явное описание каждой
переменной, используемой в регионе.
Применяется с директивой parallel.
|
reduction
(оператор:список)
|
Ограничивает доступ к оригиналам переменных. В нитях используются копии,
а значения оригиналов корректируются после выполнения каждой нити.
Ограниченные переменные должны использоваться в качестве операндов
указанного перед их списком оператора.
Применяется с директивами parallel, for, sections.
|
nowait
|
Отменяет барьер, установленный по умолчанию, поэтому после окончания
выполнения нити не происходит ожидания окончания выполнения других нитей.
Применяется с директивами for, sections, single.
|
if (условие)
|
Если условие выполнено (true), то регион распараллеливается, в противном
случае (false) все его блоки выполняются одним процессором в естественном
порядке.
Применяется с директивой parallel.
|
ordered
|
Указывает на то, что в теле директивы for будет использована
директива ordered, для выделения блока, в котором итерации
выполняются в естественном порядке.
Применяется с директивой for.
|
schedule (static |
dynamic | guided |
runtime)
|
При распараллеливании цикла данный параметр позволяет выбрать примерно
одинаковое или монотонно уменьшающееся количество итераций в нитях и способ
распределения копий цикла между нитями.
Применяется с директивой for.
|
copyin (список)
|
Для каждой нити параллельного региона создаются копии переменных,
описанных в директиве threadprivate, копиям присваиваются текущие
значения оригиналов. Имена, указанные в списках директивы threadprivate
и параметра copyin, должны совпадать.
Применяется с директивой parallel.
|
Следующие два
параметра отсутствуют в оригинальном документе,
но они предусмотрены стандартом OpenMP для C/C++.
copyprivate (список)
|
После выполнения нити, содержащей конструкцию single, новые
значения переменных списка copyprivate будут доступны всем
одноименным частным переменным (private и firstprivate),
описанным в начале параллельного региона и используемым всеми его нитями.
Применяется с директивой single.
|
num_threads
(целочисленное
выражение)
|
Предназначен для явного задания количества нитей, которое может
содержать параллельный регион. По умолчанию выбирается последнее значение,
установленное с помощью функции omp_set_num_threads, или значение
переменной omp_num_threads.
Применяется с директивой parallel.
|
3.
Переменные окружения
Стандарт OpenMP и его реализация для Intel C++ предусматривают
существование четырех переменных окружения (environment variables).
Они содержат статически определенные величины, которые могут использоваться
при формировании структуры параллельного региона. Имена переменных набираются
заглавными латинскими буквами, а аргументы могут набираться как на верхнем,
так и на нижнем регистре. Способ установки значений переменных зависит от
конкретной реализации компилятора, например, в описании стандарта сказано,
что для Unix оболочки C применяется такой способ: setenv OMP_SCHEDULE
"dynamic". Изменение значений переменных при выполнении задания
невозможно.
Переменные и их назначение перечислены в следующей таблице:
Переменная
|
Описание
|
OMP_SHEDULE
|
Содержит тип планирования и количество итераций выполняемых в нитях.
Вызывается, например, если для директивы for указан параметр shedule
(runtime).
Значение по умолчанию: static
|
OMP_NUM_THREADS
|
Задает количество нитей в параллельном регионе. Может использоваться,
например, если в директиве parallel не указан параметр num_threads.
Значение по умолчанию: количество процессоров в системе.
|
OMP_DYNAMIC
|
Разрешает/запрещает динамическое регулирование количества нитей в
параллельном регионе.
Значение по умолчанию: false.
|
OMP_NESTED
|
Разрешает или запрещает распараллеливание вложенных (внутренних) циклов.
Значение по умолчанию: false.
|
4. Функции
библиотеки runtime
В описании стандарта OpenMP для С/C++ перечислены 17 функций, входящих в
состав библиотеки runtime. Их состав и точное количество в реализации Intel
C++ мне не известны, поэтому ниже описаны только 4 функции, которые
обязательно должны быть доступны. Они позволяют в процессе выполнения задания
определить и изменить количество нитей, используемых в параллельном регионе,
определить номер конкретной нити и количество доступных процессоров в
системе.
Для возможности использования библиотечных функций к приложению должен
быть подключен заголовочный файл omp.h. Все четыре функции
целочисленные.
Имена и назначение функций перечислены в следующей таблице:
Функция
|
Описание
|
omp_get_num_threads
|
Возвращает фактическое количество нитей, существующих в параллельном
регионе. При вызове из последовательного региона возвращает значение 1.
Формат: int omp_get_num_threads(void);
|
omp_set_num_threads
|
Устанавливает количество нитей, которое может содержать параллельный
регион, имеет приоритет над переменной окружения OMP_NUM_THREADS. Для
использования функции должно быть разрешено изменение количества нитей в
процессе выполнения задания.
Формат: int omp_set_num_threads(int NumThreads);
|
omp_get_thread_num
|
Возвращает номер нити из которой вызвана функция. Номер изменяется в
пределах от 0 до omp_get_num_threads() - 1. Для основной нити (master
thread) он всегда равен нулю.
Формат: int omp_get_thread_num(void);
|
omp_get_num_procs
|
Возвращает количество процессоров, доступных для использования нитями
параллельного региона, включая логические процессоры, если поддерживается
технология Hyper-Threading.
Формат: int omp_get_num_procs(void);
|
|