Programming world map

Используемые ресурсы:

  • Тим Мэттсон. Лекции на YouTube
  • Дальнейшее чтение по адресу openmp.org: вступление

OpenMP в двух словах

OpenMP-это библиотека для параллельного программирования в модели SMP (симметричные мультипроцессоры или процессоры с общей памятью). При программировании с OpenMP все потоки совместно используют память и данные. OpenMP поддерживает языки C. C++ и Fortran. Функции OpenMP включены в заголовочный файл под названием omp.h .

Структура программы OpenMP: Программа OpenMP имеет последовательные и параллельные разделы.

В общем случае программа OpenMP начинается с последовательного раздела. В котором она устанавливает среду. Инициализирует переменные и т. Д.

При запуске программа OpenMP будет использовать один поток (в последовательных разделах) и несколько потоков (в параллельных разделах).

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

Раздел кода. Который должен выполняться параллельно. Помечается специальной директивой (omp pragma). Когда выполнение достигает параллельного участка (отмеченного прагмой omp). Эта директива вызывает формирование подчиненных потоков.

Каждый поток выполняет параллельную часть кода независимо. Когда поток заканчивается. Он присоединяется к мастеру. Когда все потоки заканчиваются. Мастер продолжает работу с кодом. Следующим за параллельным участком.

Каждый поток имеет прикрепленный к нему идентификатор. Который может быть получен с помощью функции библиотеки времени выполнения (называемой omp_get_thread_num()). Идентификатор главного потока равен 0.

Почему OpenMP? Более эффективный и низкоуровневый параллельный код возможен. Однако OpenMP скрывает низкоуровневые детали и позволяет программисту описывать параллельный код с помощью высокоуровневых конструкций. Что максимально просто.

OpenMP имеет директивы. Которые позволяют программисту:

  • укажите параллельную область
  • укажите, являются ли переменные в параллельном разделе частными или общими
  • укажите, как/если потоки синхронизируются
  • укажите, как распараллеливать циклы
  • укажите, как работы делятся между потоками (планирование)

Компиляция и выполнение кода OpenMP

Функции OpenMP включены в заголовочный файл под названием omp.h . На общедоступных linux-машинах dover и foxcroft установлен gcc/g++ с поддержкой OpenMP. Все, что вам нужно сделать. Это использовать флаг-fopenmp в командной строке:

gcc -fopenmp hellosmp.c -o hellosmp 

Кроме того. Довольно легко заставить OpenMP работать на Mac. Быстрый поиск в Google показывает. Что родной компилятор apple clang установлен без поддержки openmp. Когда вы устанавливали gcc. Он, вероятно. Был установлен без поддержки openmp. Чтобы проверить это. Зайдите в терминал и попробуйте что-нибудь скомпилировать:

gcc -fopenmp hellosmp.c -o hellosmp 

Если вы получаете сообщение об ошибке, говорящее. Что “omp.h” неизвестен. То ваш компилятор не имеет поддержки openmp.

hellosmp.c:12:10: фатальная ошибка: файл 'omp.h' не найден #include  ^ 1 сгенерирована ошибка. make: *** [hellosmp.o] Ошибка 1 

Вот что я сделал:

1. Я установил Homebrew. Отсутствующий менеджер пакетов для macOS. Http://brew.sh/index.html

/usr/bin/ruby -e 

2. Затем я попросил brew установить gcc:

 brew install gcc 

3. Затем введите

$gcc gcc gcc-6 gcc-ar-6 gcc-nm-6 gcc-ranlib-6 gccmakedep 

4. Очевидное предположение здесь заключается в том. Что gcc-6-это последняя версия. Поэтому я использую ее для компиляции:

gcc-6 -fopenmp hellosmp.c 

Работает!

Указание параллельной области (создание потоков)

Основная директива такова:


Когда главный поток достигает этой строки. Он разветвляет дополнительные потоки для выполнения работы. Заключенной в блок после конструкции #pragma. Блок выполняется всеми потоками параллельно. Исходный поток будет обозначен как основной поток с идентификатором потока 0.

Пример (программа На языке Си): Отображение

int main(void) { #pragma omp parallel { 

Используйте флаг -fopenmp для компиляции с помощью gcc:

$ gcc -fopenmp hello.c -o hello 

Вывод на компьютер с двумя ядрами и. Следовательно. Двумя потоками:

Здравствуй, мир. Здравствуй, мир. 

На Дувре я получил 24 приветствия за 24 темы. На моем рабочем столе я получаю (только) 8. Сколько вы получаете?

Обратите внимание. Что все потоки записывают стандартный вывод. И существует гонка. Чтобы поделиться им. Способ чередования потоков совершенно произволен. И вы можете получить искаженный вывод:

Привет, Велло. Вурлд. рлд. 

Частные и общие переменные

В параллельном разделе переменные могут быть частными или общими:

  • private: переменная является частной для каждого потока. Что означает. Что каждый поток будет иметь свою собственную локальную копию. Частная переменная не инициализируется и значение не поддерживается для использования вне параллельной области. По умолчанию счетчики итераций цикла в конструкциях цикла OpenMP являются закрытыми.
  • общий доступ: переменная является общей. Что означает. Что она видна и доступна всем потокам одновременно. По умолчанию все переменные в области общего доступа к работе являются общими. За исключением счетчика итераций цикла. Общие переменные должны использоваться с осторожностью. Поскольку они вызывают условия гонки.

Тип переменной. Private или shared. Указывается после #pragma omp:

Пример:

int main (int argc. Char *argv[]) { int th_id. Nthreads; #pragma omp parallel private(th_id) //th_id объявлен выше. Он указан как частный; поэтому каждый 

Личное или общее? Иногда ваш алгоритм требует совместного использования переменных. А иногда-частных. Предостережение при совместном использовании-это условия гонки. Задача продумать детали параллельного алгоритма и указать тип переменных лежит. Конечно же. На программисте.

Синхронизация

OpenMP позволяет указать. Как синхронизировать потоки. Вот что доступно:

  • критично: заключенный блок кода будет выполняться только одним потоком одновременно. А не одновременно несколькими потоками. Он часто используется для защиты общих данных от условий гонки.
  • атомарное: обновление памяти (запись или чтение-изменение-запись) в следующей инструкции будет выполняться атомарно. Это не делает все утверждение атомарным; атомарным является только обновление памяти. Компилятор может использовать специальные аппаратные инструкции для лучшей производительности. Чем при использовании критических.
  • упорядоченный: структурированный блок выполняется в том порядке. В котором итерации будут выполняться в последовательном цикле
  • барьер: каждый поток ждет. Пока все остальные потоки команды не достигнут этой точки. Конструкция совместного использования работы имеет неявную барьерную синхронизацию в конце.
  • nowait: указывает. Что потоки. Выполняющие назначенную работу. Могут продолжать работу. Не дожидаясь завершения всех потоков в команде. В отсутствие этого предложения потоки сталкиваются с барьерной синхронизацией в конце конструкции совместного использования работы.

Подробнее о барьерах: Если бы мы хотели. Чтобы все потоки были в определенной точке их выполнения. Прежде чем продолжить. Мы бы использовали барьер. Барьер в основном говорит каждому потоку:

Пример барьера:

int main (int argc. Char *argv[]) { int th_id. Nthreads; #pragma omp parallel private(th_id) { th_id = omp_get_thread_num(); printf(}//главная 

Обратите внимание на приведенную выше функцию omp_get_num_threads(). Можете ли вы догадаться. Что он делает? Некоторые другие функции времени выполнения:

  • omp_get_num_threads
  • omp_get_num_procs
  • omp_set_num_threads
  • omp_get_max_threads

Распараллеливание циклов

Распараллеливание циклов с помощью OpenMP очень просто. Один просто обозначает цикл для распараллеливания и несколько параметров. А OpenMP заботится обо всем остальном. Не может быть проще!

Директива называется конструкцией совместного использования работыи должна быть помещена в параллельный раздел:

#pragma omp for //укажите цикл for для распараллеливания; без фигурных скобок 

Функция “#pragma omp for” распределяет цикл между потоками. Он должен использоваться внутри параллельного блока:

#pragma omp parallel { … #pragma omp for //для распараллеливания цикла … }//конец параллельного блока 

Пример:

//вычислить сумму двух массивов параллельно #define N 1000000 int main(void) { float a[N], b[N]. C[N]; int i; /* Инициализация массивов a и b */ для (i = 0; i) { a[i] = i * 2.0; /* Вычислите значения массива c = a+b параллельно. */ #pragma omp parallel shared(a. Brivate(i) { #pragma omp for for (i = 0; i) { c[i] = a[i] + b[i]; 

Другой пример (здесь): добавление всех элементов в массив.

//example4.c: добавление всех элементов в массив параллельно int main() { const int N=100; int a[N]; //initialize for (int i=0; i) a[i] = i; //compute sum int local_sum. Sum; #pragma omp parallel private(local_sum) shared(sum) { local_sum =0; //массив распределяется статически между потоками #pragma omp for schedule(static,1) для (int i=0; i) { //каждый поток вычислял свой local_sum. Все потоки должны добавить к //глобальная сумма. Крайне важно. Чтобы эта операция была атомной. 

Существует также директива “parallel for”. Которая сочетает в себе параллель и a for (нет необходимости вставлять a for внутри параллели):

int main(int argc. Char **argv) { int a[100000]; #pragma omp parallel for for (int i = 0; i) { 

Точно. Как итерации назначаются потоку ecah. Что указано в расписании (см. Ниже). Примечание:Поскольку переменная i объявлена внутри параллели for. Каждый поток будет иметь свою собственную частную версию i.

Планирование цикла

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

  • статика: Каждому потоку назначается блок итераций фиксированным образом (циклический перебор). Итерации делятся между потоками поровну. Указание целого числа для параметра chunk выделит частичному потоку количество непрерывных итераций. Примечание: это значение по умолчанию? проверять.
  • динамический: Каждый поток инициализируется куском потоков, а затем. Когда каждый поток завершает свои итерации. Ему назначается следующий набор итераций. Параметр chunk определяет количество непрерывных итераций. Выделяемых потоку за один раз.
  • управляемый: Итерации делятся на части. Которые последовательно уменьшаются экспоненциально. Причем кусок является наименьшим размером.

Это задается добавлением schedule(type, chunk) после директивы pragma for:

#pragma omp for schedule(static, 5) 

Более сложные директивы

…который вам, вероятно. Не понадобится.

  • можно определить “секции” внутри параллельного блока
  • может запросить. Чтобы итерации цикла выполнялись по порядку
  • укажите блок. Который будет выполняться только главным потоком
  • укажите блок который будет выполняться только первым потоком достигшим его
  • определите раздел как “критический”: он будет выполняться каждым потоком. Но может выполняться только одним потоком одновременно. Это заставляет нити идти по очереди. А не перебивать друг друга.
  • определите секцию как “атомарную”: это заставляет потоки записывать данные в общую ячейку памяти последовательным образом. Чтобы избежать условий гонки
int main(void) { int count = 0; #pragma omp parallel shared(count) { 

Соображения производительности

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

Некоторые комментарии

OpenMP-это не магия. Цикл должен быть явно распараллеливаемым. Чтобы OpenMP мог развернуть его и облегчить назначение итераций между потоками. Если существуют какие-либо зависимости данных от одной итерации к другой. То OpenMP не может распараллелить их.

Например, цикл for не может выйти рано:

// BAD - can;t распараллелить с OpenMP for (int i=0;i) { 

Значения управляющих выражений цикла должны быть одинаковыми для всех итераций цикла. Например:

// BAD - can;t распараллелить с OpenMP for (int i=0;i) { if (i = = 50)