image

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

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

1. Именуйте критические секции

В очередь, сукины дети, в очередь! //М. А. Булгаков «Собачье сердце»

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

#pragma omp critical [(имя)] новая строка   
структурированный блок


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

Пример:

#pragma omp critical (first) 
{
    workA();
}
#pragma omp critical (second) 
{
    workB();
}
//секции с workA() и workB() будут выполнены одновременно 

#pragma omp critical  
{
    workC();
}
#pragma omp critical 
{
    workD();
}
// будет завершена секция с  workC() , а только потом  с workD()


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

2. Не стоит использовать != в управлении циклом

Директива for накладывает ограничения на структуру соответствующего цикла. Определенно, соответствующий цикл должен иметь каноническую форму.

image

Ответ разработчиков из OpenMP Architecture Review Board

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

Для простого цикла как:
for( i = 0; i < n; ++i )

можно определить количество итераций, n если n>=0, и ноль итераций если n < 0.

for( i = 0; i != n; ++i ) 

можно определить n итераций, если n>=0; если n < 0, мы не знаем количество итераций.

for( i = 0; i < n; i += 2 )

число итераций целая часть от ((n+1)/2) если n >= 0, и 0 если n < 0.

for( i = 0; i != n; i += 2 )

не можем определить когда i равняется n. Что если n нечетное число?

for( i = 0; i < n; i += k )

количество итераций наибольшее целое от ((n+k-1)/k) если n >= 0, и 0 если n < 0; в случае, если k < 0, это не допустимая OpenMP программа.

for( i = 0; i != n; i += k )

i увеличивается или уменьшается? Будет ли равенство? Это все может привести к бесконечному циклу.

3. Внимательно устанавливайте nowait

Если Хатико хочет ждать, он должен ждать.// Хатико: Самый верный друг

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

Пример:
#pragma omp parallel shared(n,a,b,c,d,sum) private(i) schedule(dynamic)
{
   #pragma omp for nowait
   for (i = 0; i < n; i++)
   a[i] += b[i];

   #pragma omp for nowait
   for (i = 0; i < n; i++)
   c[i] += d[i];

   #pragma omp for nowait reduction(+:sum)
   for (i = 0; i < n; i++)
   sum += a[i] + c[i];
}


Данном примере есть ошибка, она заключается в schedule(dynamic). Дело в том, что зависимых по данным циклы nowait допустим только c schedule(static). Только в этом способе планирования работ стандарт гарантирует корректную работу c nowait для циклов зависимых по данным. В нашем случае достаточно стереть schedule(dynamic) в большинстве реализаций по умолчанию используется schedule(static).

4. Тщательно проверьте код перед использованием task untied

int dummy;

#pragma omp threadprivate(dummy)
void foo() {dummy = …; }
void bar() {… = dummy; }

#pragma omp task untied
{
    foo();
    bar();
}

task untied специфицирует, что задача не привязана к потоку, который начал ее исполнять. Другой поток может продолжить исполнение задачи после приостановки. В данном примере неправильное использование task untied. Программист предполагает, что обе функции в задаче будут исполнены одним потоком. Однако, если после приостановки задачи bar() будет выполнен другим. По причине того, что у каждого потока своя переменная dummy (в нашем случае она threadprivate ). Присвоение в bar() произойдет некорректное.

Надеюсь, данные советы помогут начинающим.

Полезные ссылки:
Примеры OpenMP 4.0 pdf
Примеры OpenMP 4.0 github
Стандарт OpenMP 4.0 pdf
Все директивы на 4 листах C++ pdf
Все директивы на 4 листах Fortran pdf
Одни из лучших слайдов по OpenMP на русском языке от Михаила Курносова pdf

Комментарии (4)


  1. fshp
    31.05.2015 02:39
    +2

    Не стоит использовать != в управлении циклом

    OpenMP в основном предназначается для параллельной обработки данных. Любая порция данных имеет определённый размер. Стандарт C++ вводит специальный тип данных «size_t» для хранения размеров. По определению он не может быть отрицательным. Так что главное правило — используйте правильные типы данных. Не нужно писать
    for(int i = 0...
    если вы хотите проитерировать контейнер. Это не только будет легче читаться, но и упростит задачу компилятору.

    P.S. конечно и итерация отрицательных чисел иногда нужна бывает. Но большинство задач сводятся к векторизации обработки массивов.


  1. morgen2009
    31.05.2015 17:07

    Кто знает почему под C# не сделали OpenMP реализацию? Там вроде есть параллелизм данных, но скорость на поток резко падает по сравнению с аналогичным циклом в Фортране?


    1. fshp
      31.05.2015 20:42
      +1

      Может потому, что в C# есть сборщик мусора? И на каждый чих память выделяется из кучи. Всё же для числодробилок есть языки с ручным управлением памятью.


      1. VenomBlood
        31.05.2015 22:59

        Выделение памяти крайне быстрое, а вот сборка мусора — да, медленно.