2.1 Базовые понятия OpenMP

Введение

OpenMP (Open specifications for Multi–Processing) – стандарт для написания параллельных программ для многопроцессорных вычислительных систем с общей оперативной памятью. Программа представляется как набор нитей (threads), объединённых общей памятью, где проблема синхронизации решается введением критических секций и мониторов.

Стандарт OpenMP был разработан в 1997 году, как API, ориентированный на написание портируемых многопоточных приложений. Сначала он был основан на языке Fortran, но позднее включил в себя и C/C++.

Разработкой стандарта занимается организация OpenMP ARB (ARchitecture Board), в которую вошли представители крупнейших компаний – разработчиков SMP-архитектур и программного обеспечения. Спецификации для языков Fortran и C/C++ появились соответственно в октябре 1997 года и октябре 1998 года. OpenMP задуман как стандарт для программирования на масштабируемых SMP-системах (SSMP, ccNUMA, etc.) в модели общей памяти (shared memory model). На данный момент последняя официальная спецификация стандарта – OpenMP 3.1 (принятая в июле 2011 года).

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

За счет идеи “частичного распараллеливания” OpenMP идеально подходит для разработчиков, желающих быстро распараллелить свои вычислительные программы с большими параллельными циклами. Разработчик не создает новую параллельную программу, а просто добавляет в текст последовательной программы OpenMP директивы. Предполагается, что OpenMP-программа на однопроцессорной платформе может быть использована в качестве последовательной программы, т.е. нет необходимости одновременно поддерживать последовательную и параллельную версии. Директивы OpenMP просто игнорируются последовательным компилятором, а для вызова процедур OpenMP могут быть подставлены заглушки (stubs), текст которых приведен в спецификациях.

OpenMP прост в использовании и включает лишь два базовых типа конструкций: директивы pragma и функции исполняющей среды OpenMP. Директивы pragma, как правило, указывают компилятору, как реализовать параллельное выполнение блоков кода. Все эти директивы начинаются с фразы pragma omp. Как и любые другие директивы pragma, они игнорируются компилятором, не поддерживающим конкретную технологию – в данном случае OpenMP. Каждая директива может иметь несколько дополнительных атрибутов. Отдельно специфицируются атрибуты для назначения классов переменных, которые могут быть атрибутами различных директив.

Функции OpenMP служат в основном для изменения и получения параметров окружения. Кроме того, OpenMP включает API-функции для поддержки некоторых типов синхронизации. Чтобы задействовать эти функции OpenMP библиотеки периода выполнения (исполняющей среды), в программу нужно включить заголовочный файл omp.h. Если же используется в приложении только OpenMP-директивы pragma, включать этот файл не требуется.

Модель ветвление-слияние

OpenMP используется модель параллельного выполнения “ветвление-слияние” (fork-join). Программа начинается выполнением одной нити, называемой начальной (initial) нитью. Начальная нить выполняется последовательно. Когда нить достигает директивы parallel она создает команду нитей, состоящую из неё самой и нуля или более дополнительных нитей, и становится хозяйкой (master) созданной команды. Все члены команды исполняют код структурной области, связанной с директивой parallel (параллельной области). В конце параллельной области размещается неявный барьер. Только нить-хозяйка продолжает выполнение после завершения параллельной области.

Число нитей в команде, выполняющихся параллельно, можно контролировать несколькими способами. Один из них – использование переменной окружения OMP_NUM_THREADS. Другой способ – вызов процедуры omp_set_num_threads(). Еще один способ – использование выражения num_threads в сочетании с директивой parallel.

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

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

Конструкции синхронизации и библиотечные подпрограммы позволяют согласовывать выполнение нитей и доступ к данным в параллельных областях. Отметьте, что нет гарантий синхронного доступа к файлам при выполнении ввода/вывода из разных нитей, поэтому синхронизация в этом случае возлагается на программиста.

Модель памяти

OpenMP предполагает, что программа выполняется в системе с разделяемой памятью, в которой хранятся (могут быть сохранены и выбраны) переменные программы доступные всем её нитям. Кроме того каждая нить имеет доступ к памяти, недоступной для всех других нитей; такая память называется частной памятью нити.

Директива parallel разделяет память, доступную параллельной области, на разделяемую (shared) и частную (private). Каждая переменная, встречающаяся в параллельной области, имеет соотвествующую одноименную (оригинальную) переменную вне параллельной области. Разделяемая переменная ссылается на ту же память, что и оригинальная переменная вне параллельной области. Для каждой частной переменной создается новая переменная в памяти каждой нити, кроме, быть может, нити-хозяйки. Частные переменные в параллельной области ссылаются на частную память нити.

Дополнительно каждая нить может иметь своё временное представление памяти. Это временное представление не является необходимой частью модели памяти OpenMP, но учитывает наличие в современных вычислительных системах регистров процессора, кэшей различных уровней и других структур, позволяющих нити кэшировать переменные и избегать лишних обращений к памяти. Это временное представление не обязано быть все время согласованным с памятью. Поэтому OpenMP предоставляет средства позволяющие принудительно установить согласованность временного представления с памятью. Предполагается, что компилятор C/C++ поддерживающий OpenMP, автоматически поддерживает согласованность разделяемых переменных, описанных с ключевым словом volatile. Естественно предполагается, что для таких переменных проводится принудительное согласование непосредственно перед чтением и непосредственно после записи.

Компиляторы не ограничиваются в переупорядочивании операций нити с памятью и использовании преимуществ временного представления кроме необходимости сохранять порядок принудительных согласований. Поскольку согласование могут выполнять различные нити, OpenMP предполагает слабое упорядочение согласований.

Компиляция

Для использования механизмов OpenMP нужно скомпилировать программу компилятором, поддерживающим OpenMP, с указанием соответствующего ключа например:

icc/ifort используется ключ компилятора -openmp 
gcc /gfortran -fopenmp 
Sun Studio -xopenmp 
Visual C++ - /openmp 
PGI -mp

Компилятор интерпретирует директивы OpenMP и создаёт параллельный код. При использовании компиляторов, не поддерживающих OpenMP, директивы OpenMP игнорируются без дополнительных сообщений. Компилятор с поддержкой OpenMP определяет макрос _OPENMP, который может использоваться для условной компиляции отдельных блоков, характерных для параллельной версии программы. При необходимости это определение можно протестировать, как показано ниже:

#ifdef _OPENMP 
    fn(); 
#endif