bigpo.ru
добавить свой файл
1


Глава 4 Параллельное программирование с использованием OpenMP


В данной главе описывается технология параллельного программирования OpenMP для систем вычислительных с общей памятью.

OpenMP - прикладной программный интерфейс (API) для создания параллельных приложений для МВС, использующих общую память. При реализации OpenMP основной упор делался на создание системы программирования, которая позволила облегчить параллельное программирование задач для систем с общей памятью.

Стандарт OpenMP предназначен, для выполнения параллельной программы на широком круге систем с архитектурой SMP, за счет поддержки данного стандарта производителями суперкомпьютеров и создания реализаций стандарта для каждой вычислительной платформы. Развитие многоядерных и многопоточных процессоров позволяет использовать OpenMP, для создания программ для однопроцессорных компьютеров.

OpenMP не является новым языком программирования, это нотации (директивы), которые могут быть добавлены к последовательной программе в ФОРТРАНе, C или C++, чтобы описать, как вычисления должна быть разделены среди нитей (threads), которые выполнятся на различных процессорах или ядрах и разграничить доступ к общим данным. Вставка соответствующих директив OpenMP в последовательную программу позволит увеличить производительность выполняемой программы на параллельной архитектуре с общей памятью при минимальной модификации кода.

При подготовке главы были использованы работы российских и зарубежных ученых. Стандарт OpenMP описан группой разработчиков в Спецификации OpenMP [16]. В качестве основных русскоязычных источников, посвященных технологии OpenMP, можно рассматривать работы: Антонова А. С. [1], Гергеля В. П. [2], Лупина С. А. [3], Левина М. П. [4], в качестве англоязычных работ: Chandra R. [14], Charman B. [15]. Так же информация о OpenMP представлена на информационных порталах по тематике параллельных вычислений, например, [6-13].

При изложении материала описание директив и функции OpenMP приводится для языков программирования Си, Си++ и Фортран. Примеры же в основном приводятся на языке Си, кроме тех случаев, когда используются принципиально разные подходы в Си и Фортране.

Для удобства использования в учебном процессе материал изложен в виде лабораторных работ, рассчитанных на 1 или 2 академических часа. Каждая лабораторная заканчивается списком вопросов для самоконтроля и закрепления материала, списком заданий для выполнения на занятии и списком заданий для самостоятельного выполнения.


Лабораторная работа №1. Компиляция и запуск OpenMP


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


Модель параллельного программирования OpenMP


Стандарт OpenMP применяется при создании параллельных приложений для вычислительных систем с общей памятью. В модели OpenMP предполагается создание многопоточного (многонитевого) приложения и решается задача распределения нитей между процессорами вычислительной системы. Эффективность программы в этом случае зависит от того, насколько эффективно распределяется нагрузка между нитями.

Стандарт OpenMP отличается от MPI тем, что в последнем, программист явно указывает какие данные с какого процесса и на какой должны быть отправлены. В OpenMP реализация функций скрыта от программиста, он с помощью набора директив указывает, как должны быть распределены данные, библиотека OpenMP реализует функции по распределению.


Модель запуска программ OpenMP


Стандарт OpenMP реализуется на уровне компилятора языков высокого уровня Фортран, Си и Си ++, с помощью набора директив. Если компилятор не поддерживает стандарт OpenMP, то программа компилируется как обычное последовательное приложение.

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

На рис. 1 показан порядок выполнения параллельной программы OpenMP. Главная нить (master thread) выполняет блок последовательного кода, директивой parallel создает группу нитей (teams of threads), далее блок параллельного кода выполняется всеми нитями в группе. После выполнения параллельного кода группа нитей завершает свою работу, и последовательную часть выполняет опять главная нить.




Рис. 1 Модель запуска OpenMP


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


В OpenMP предполагается наличие как общей для всех нитей области памяти, так и локальной области памяти для каждой нити. Общая переменная всегда существует лишь в одном экземпляре для всей области действия и доступна всем нитям под одним и тем же именем. Объявление локальной переменной вызывает порождение своего экземпляра данной переменной (того же типа и размера) для каждой нити. Изменение нитью значения своей локальной переменной никак не влияет на изменение значения этой же локальной переменной в других нитях.

Если несколько переменных одновременно записывают значение общей переменной без выполнения синхронизации или если как минимум одна нить читает значение общей переменной и как минимум одна нить записывает значение этой переменной без выполнения синхронизации, то возникает ситуация так называемой «гонки данных» (data race), при которой результат выполнения программы непредсказуем.


Создание OpenMP программы

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

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

Директивы OpenMP позволяют программисту говорить компилятору, какие команды должны выполняться параллельно и как их распределять среди нитей, которые выполнят код. Директива OpenMP – это команда в специальном формате, которая понятна только компиляторам OpenMP. Фактически, это выглядит как комментарий для компилятора ФОРТРАН или псевдокомментарий для компилятора C/C ++, это сделано для того, чтобы программа могла выполниться даже, если компилятор не знает директив OpenMP. Количество директив стандарта OpenMP невелико, по сравнению с другими стандартами на параллельные библиотеки (MPI, DVM), но их функциональных возможностей хватает для обеспечений потребностей разработчиков программного обеспечения.

Функции позволяют управлять параметрами и замерять время выполнения OpenMP программ.


Директивы


Распараллеливание OpenMP реализуется с помощью директив компилятору. Они указывают, как должна выполняться параллельная программа. Директивы OpenMP для языка Си/Си++ задаются директивами предварительной обработки pragma.


Синтаксис директив OpenMP. Язык Си

#pragma omp directive-name [clause[[,]clause]] new-line


Все директивы начинаются с #pragma omp. Оставшаяся часть директивы записывается в соответствии с соглашениями стандарта Си/Си++ для директив компиляции. Предварительная обработка (препоцессинг) следующего маркера #pragma omp выполняет подстановку макрокоманды. Директивы зависят от регистра ввода команд. Исполняемые директивы OpenMP применяется к одному структурированному блоку.


Директивы OpenMP для языка Фортран задаются следующим образом:


Синтаксис директив OpenMP. Язык Фортран

Sentinel directive-name [clause[ [,] clause]…]


Только одна команда directive-name может быть использована при описании директивы (кроме случая использования комбинированных директив, о которых будет рассказано позднее). Порядок записи clauses в директиве не существенен. В директиве clause может повторяться по мере необходимости, список ограничений использования описывается к каждой clause.

Все OpenMP директивы компилятора должны начинаться с комментария sentinel. Формат метки sentinel различается для исходных файлов с фиксированной и свободной формой ввода. Для языка Фортран директивы не зависят от регистра ввода команд. В порядке упрощения представления материала, используется свободная форма для записи синтаксиса директив OpenMP на языке Фортран.


Формат метки sentinel в фиксированной форме. Язык Фортран

!$omp | c$omp | *$omp


Метки должны начинаться с первой позиции в строке и выглядеть как единое слово без промежуточных символов. В Фортране фиксируется длина строки, включая пробелы, символы переноса и эти правила распространяются и на строку директивы. Директива должна начинаться с 6-ой позиции в строке, для того, чтобы выполнить перенос директивы на следующую строку необходимо в 6-ой позиции выставить знак переноса.


Пример трех эквивалентных форматов записи директив. Язык Фортран

!$omp parallel do shared(a,b,c)


c$omp parallel do

c$omp+shared(a,b,c)


c$omp paralleldoshared(a,b,c)




Формат метки sentinel в свободной форме. Язык Фортран

!$omp


Метка может располагаться в любой позиции строки, отступ задается символами пробел или табуляция. Метка должны быть записана как одно слово без пробелов. К строке директивы применяются правила Фортрана: длина строка, пробелы, правила переноса. В строке директивы после метки обязательно следует пробел. Для разделения длинной строки директивы в конце строки ставиться амперсанд.


Пример записи директив в свободной форме. Язык Фортран

!$omp parallel do &

!$omp shared(a,b,c)


!$omp parallel &

!$omp&do shared(a,b,c)


!$omp paralleldo shared(a,b,c)


Директивы OpenMP можно разделить на 3 категории: определение параллельной области, распределение работы, синхронизация. Каждая директива может иметь несколько дополнительных атрибутов – опций (clause). Отдельно специфицируются опции для назначения классов переменных, которые могут быть атрибутами различных директив.

В качестве примера рассмотрим описание директивы создания параллельной области. Дериктива parallel – инициализирует параллельную область и создает группу нитей. Полное описание директивы будет приведено в следующем параграфе.

Синтаксис оператора. Язык Си

#pragma omp parallel [опция[[,] опция]...]




Синтаксис оператора. Язык Фортран

!$omp parallel [опция[[,] опция]...]

<код параллельной области>

!$omp end parallel

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


Функции


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

Имена все функций OpenMP начинаются с префикса omp_. Если имя функции начинается omp_set_, то это означает, что функция изменяет выбранный параметр. Если имя функции начинается omp_get_, то это означает, что функция считывает значение параметра. Почти все функции OpenMP являются парными функциями.

Если пользователь не будет использовать в программе имён, начинающихся с такого префикса, то конфликтов с объектами OpenMP заведомо не будет. В языке Си, кроме того, является существенным регистр символов в названиях функций. Названия функций OpenMP записываются строчными буквами.

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

Функции OpenMP можно разделить на следующие блоки:

- функции исполняющей среды;

- функции блокировки;

- функции определения времени;

- функции изменения среды выполнения параллельной программы: вложенный параллелизм, динамическая корректировка потоков;


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

- функция omp_get_num_threads() возвращает количества нитей;

- функция omp_set_num_threads() задает количество используемых нитей;

- функция omp_get_max_threads() возвращает максимально допустимого количества нитей;

- функция omp_get_thread_num() возвращает номер нити;

- функция omp_get_thread_limit() возвращает максимально допустимое количество нитей;

- функция omp_get_num_procs() возвращает количества доступных для использования процессоров;

- функция omp_in_parallel() возвращает нахождения в параллельной области;


В OpenMP реализованы простые и вложенные функции блокировки. Вложенные блокировки помечаются префиксом «nest». Выполняются блокировки с помощью переменных блокировки типа omp_lock_t и omp_lock_nest_t. Переменная блокировки может находиться в одном из следующих состояний: инициалирована, неинициалирована, заблокирована, разблокирована. Управление переменными блокировки осуществляется следующим набором функций:

- функция инициализации простой блокировки omp_init_lock();

- функция перевода простой блокировки в неинициализированное состояние omp_destroy_lock();

- функция захвата переменной простой блокировки omp_set_lock();

- функция снятия простой блокировки omp_unset_lock();

- функция, реализующая неблокирующую проверку и попытку захвата простой блокировки omp_test_lock();

- функция инициализации вложенной блокировки omp_init_nest_lock();

- функция перевода вложенной блокировки в неинициализированное состояние omp_destroy_nest_lock();

- функция захвата переменной вложенной блокировки omp_set_nest_lock();

- функция снятия вложенной блокировки omp_unset_nest_lock();

- функция, реализующая неблокирующую проверку и попытку захвата переменной вложенной блокировки omp_test_nest_lock();


В OpenMP реализованы следующие функции работы с системным таймером:

- функция определения времени в секундах omp_get_wtime();

- функция определения разрешения системного таймера omp_get_wtick();


В блок функций среды выполнения параллельной программы входят следующие функции:

- функции задает динамическое изменение количества нитей omp_set_dynamic();

- функции определяет, используется ли динамическое изменение количества нитей omp_get_dynamic();

- функция задает вложенных параллельных областей omp_set_nested();

- функция определяет, используется ли вложенных параллельных областей omp_get_nested();

- функция omp_set_schedule() задает тип планировщика, при применении для распараллеливания циклов варианта планирования schedule(runtime);

- функция omp_get_schedule() возвращает тип планировщика, при применении для распараллеливания циклов варианта планирования schedule(runtime);

- функция omp_set_max_active_levels() задает максимальное количество активных вложенных параллельных областей;

- функция omp_get_max_active_levels() возвращает максимальное количество активных вложенных параллельных областей;

- функция omp_get_level() возвращает уровень вложенности параллельных областей;

- функция omp_get_ancestor_thread_num() возвращает номер нити (предка) для текущей нити выполняющей вложенную параллельную область;

- функция omp_get_team_size() возвращает количество нитей, которые выполняют текущую вложенную параллельную область;

- функция omp_get_active_level() возвращает уровень вложенности параллельных областей.


Полное описание функций и варианты из применения приведены в параграфе №3. Вспомогательные функции. Здесь же приводится описание только функций работы с системным таймером, так они будут использованы для оценки времени исполнения параллельных программ, рассматриваемых следующих параграфах.

Функция omp_get_wtime() возвращает в вызвавшей нити астрономическое время в секундах (вещественное число двойной точности).


Синтаксис оператора. Язык Си

double omp_get_wtime(void);




Синтаксис оператора. Язык Фортран

double precision function omp_get_wtime()


Для определения времени выполнения блока кода исходной программы, необходимо вызвать функцию omp_get_wtime() в начале блока и в конце. Разность полученных значений и будет время исполнения блока. Гарантируется, что момент времени, используемый в качестве точки отсчета, не будет изменён за время существования процесса. Таймеры разных нитей могут быть не синхронизированы и выдавать различные значения.

Функция omp_get_wtick() возвращает в вызвавшей нити разрешение таймера в секундах. Это время можно рассматривать как меру точности таймера.


Синтаксис оператора. Язык Си

double omp_get_wtick(void);




Синтаксис оператора. Язык Фортран

double precision function omp_get_wtick()


Компиляция и запуск программы


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


Параметр компиляции для разных компиляторов

Intel Си/Си++

icc first.cpp –openmp

Intel Фортран

ifort first.f90 –openmp

GNU Си/Си++

gcc/g++ first.c –fopenmp

GNU Фортран

gfortran first.f90 –fopenmp

MS Visual Си/Си++

/Qopenmp


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

- с помощью переменных окружения;

- с помощью функций задания количества нитей;

- с помощью параметров директив.

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

Количество нитей определяется значением переменной окружения OMP_NUM_THREADS.


Задание переменной окружения OMP_NUM_THREADS

Linux

export OMP_NUM_THREADS=n

Windows

set OMP_NUM_THREADS=n


Проверить значение можно с помощью команды echo.


Проверка значения переменной окружения OMP_NUM_THREADS

Linux

echo $OMP_NUM_THREADS

Windows

echo %OMP_NUM_THREADS%


Для запуска программы OpenMP не требуется специализированных исполнительных оболочек, используется стандартный загрузчик операционной системы. В Unix-системах по умолчанию название программы a.out, в Windows - по названию исходного файла. После запуска программа считывает значение переменной окружения OMP_NUM_THREADS определения количества порождаемых параллельных нитей.


Запуск параллельной OpenMP программы

Linux

./a.out

Windows

first.exe



Первая программа OpenMP


Рассмотрим пример OpenMP программы, которая выводит номер нити и количество нитей, на которых выполняется параллельная программа.


Пример 1.

Программа выводит номер нити и количество нитей. Язык Си

#include

#include

int main(){

#pragma omp parallel{

int rank=omp_get_thread_num();

int size=omp_get_num_threads();

printf("Thread %d of %d threads\n", rank, size);

}

}




Программа выводит номер нити и количество нитей. Язык Фортран

program Count_of_threads

include “omp_lib.h”

integer rank, size

!$OMP PARALLEL

size=omp_get_thread_num()

size=omp_get_num_threads()

print *, "Thread",rank, " of threads ", size

!$OMP END PARALLEL

End


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


Из общего курса по Параллельным вычислениям известно, что основным критерием оценки параллельной программы является уменьшением времени вычислений при увеличении количества используемых процессоров/ядер. Следующий пример иллюстрирует применение функций omp_get_wtime() и omp_get_wtick() для определения времени выполнения параллельной программы, написанной с использованием OpenMP. В данном примере производится замер начального времени, вычисления моделируются оператором sleep(1), затем замер конечного времени. Разность даёт время выполнения параллельной программы. Точность системного таймера измеряется функцией omp_get_wtick().


Пример 2.

Программа на языке Си определения времени выполнения блока кода

#include

#include

int main(int argc, char *argv[])

{

double start_time, end_time, tick;

start_time = omp_get_wtime();

sleep (1);

end_time = omp_get_wtime();

tick = omp_get_wtick();

printf("Время %lf\n", end_time-start_time);

printf("Таймер %lf\n", tick);

}




Программа на языке Фортран определения времени выполнения блока кода

program get_time

include "omp_lib.h"

double precision start_time, end_time, tick

start_time = omp_get_wtime()

pause 10

end_time = omp_get_wtime()

tick = omp_get_wtick()

print *, "Время ", end_time-start_time

print *, "Таймер ", tick

end


В результате выполнения программы на экран будет выведено затраченное время и разрешение системного таймера. Варьируя значением оператора sleep (pause), можно попытаться моделировать более ресурсоемкие вычисления. Вместо указанных операторов можно вставить цикл, например, суммирования ряда чисел.


Вопросы


  1. Укажите для какой архитектуры вычислительных систем применяется технология OpenMP?

  2. Выпишите формат записи директив OpenMP на языках Си и Фортран.

  3. Укажите какой параметр необходимо указывать компилятору для компиляции OpenMP программ?

  4. Укажите с помощью какой директивы порождается параллельная область?

  5. Приведите правильный формат записи переноса на новую строку директив OpenMP на языке Фортран.

  6. Выпишите команду задания количество нитей.

  7. Укажите названия функции определения времени?


Упражнения


  1. Определите, какую версию стандарта OpenMP поддерживает компилятор на доступной системе. (добавить теорию)

  2. Задайте с помощью переменных окружения количество нитей равное 5 и выполните пример номер 1.

  3. Задайте с помощью функций количество нитей равное 5 и выполните пример номер 1.

  4. По условию суммирование четных и нечетных элементов последовательности (массива) 1/(i+1). Время больше чем один тик.

    1. время вычисления.

    2. подобрать количество элементов.

  5. Придумать пример.

  6. Откомпилируйте любую последовательную программу с включением опций поддержки технологии OpenMP и запустите с использованием нескольких нитей. Сколько нитей будет реально исполнять операторы данной программы?

  7. Напишите программу замера времени выполнения директивы #pragma omp parallel . Выполните запуск программы для 1, 2 и 4 нитей. Как изменилось время выполнения параллельной программы?