Pull to refresh
0

Ждали, ждали и дождались! OpenMP 4.0

Reading time 5 min
Views 22K


Каждая новая спецификация OpenMP вводит очень полезные и необходимые дополнения к уже существующему функционалу. Например, в версии 3.0 были добавлены так ожидаемые задачи (tasks), позволившие решать ещё больший спектр задач по распараллеливанию приложений. В 3.1 целый ряд улучшений по работе с задачами и редукциями.

Но по сравнению с тем, что нам теперь даёт стандарт 4.0, предыдущие нововведения кажутся какими-то мелкими. Последняя версия расширила типы поддерживаемого параллелизма, чего раньше никогда не замечалось.

Все мы знаем, что параллелизм – он ведь очень разный. Начинается он ещё на уровне инструкций, когда современные процессорные архитектуры предоставляют нам функционал конвейера и суперскалярности. Уже на этом уровне мы можем говорить о том, что наше приложение параллельно. Но это то, что уже заложено в «железе» на сегодняшний день, и что для разработчиков используется неявно. Есть у существующих на сегодняшний день процессоров и другие «сладости» — количество ядер и векторные регистры (SSE, AVX и т.д.).

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


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

Больше того, до последнего момента, а именно июля 2013 года, речь шла всегда о параллелизме по задачам. Причём использование OpenMP стало одним из наиболее популярных методов, универсальным в плане поддержки языка (подходит как для С/С++, так и Фортрана) и простым в плане реализации (идея добавления параллельности шаг за шагом через директивы).

Но не меньшую роль в плане производительности имеет и параллелизм по данным. Важно отметить, что векторизация – один из ключевых моментов при использовании компилятора Intel, если вы работаете над приложением, для которого действительно важна высокая производительность. Кстати, и для любого другого компилятора тоже. Поэтому относительно давно существует целый набор различных директив, помогающих векторизовать код – то есть сгенерировать такие инструкции, которые будут использовать всю длину регистров, будь то SSE, AVX или AVX2, или что-то ещё более «свежее» и «длинное». Не стоит забывать, что и на новой MIC архитектуре векторизация играет ключевую роль.

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

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

Хотел бы сначала показать, как это было в Cilk Plus’е на простом примере.
Допустим, у нас есть некий цикл:

void add_floats(float *a, float *b, float *c, float *d, float *e, int n){
 int i;
 for (i=0; i<n; i++){
  a[i] = a[i] + b[i] + c[i] + d[i] + e[i];
 }
}

Так вот если мы скомпилируем его с ключом vec-report, чтобы узнать, был ли он векторизован, то увидим, что нет. Причина в консервативности компилятора – если он «чувствует», что векторизация опасна, то предпочтёт «перестраховаться» и ничего не делать. В нашем случае это происходит из-за большого количества указателей, переданных в функцию. Компилятор просто думает, что какие-то из них вполне могут иметь пересечения в памяти. Но если мы как разработчики знаем, что это не так и векторизовать этот цикл безопасно, можем использовать директиву pragma simd из Intel CilkPlus:

 #pragma simd
 for (i=0; i<n; i++){
  a[i] = a[i] + b[i] + c[i] + d[i] + e[i];

Пересобираем и вуаля – цикл векторизован! Но ещё раз – это специфично для интеловского компилятора. Теперь же такой функционал появился и в OpenMP, только директива стала в более «привычной» форме, а именно:

#pragma omp simd

Кроме того, есть и смешанная конструкция, для использования двух типов параллелизма одновременно:

#pragma omp parallel for simd

Стоит отметить, что использовать конструкцию в «голом» виде не очень рекомендовано, потому что компиляторные проверки для данного цикла полностью отключаются. Поэтому можно и нужно задавать, как и в других OMP директивах, дополнительные опции, помогающие компилятору (о длине вектора, о выравнивании памяти, о доступе к ней и т.д.). Кстати, они так же весьма похожи на те, что уже были в компиляторе Intel, но с небольшими изменениями.

Была так же добавлена и другая очень полезная вещь – так называемые элементные функции. Не секрет, что если в цикле вызывается некая функция, то в общем случае этот цикл не векторизуется. В Cilk Plus использовалось магическое __declspec(vector), позволяющее сделать из самой обычной функции элементную — ту, которая могла бы «выдавать» вектор результатов. В итоге её можно было вызывать и в цикле, с последующей его удачной векторизацией. В OpenMP это выглядит теперь так:

#pragma omp declare simd

Причём сейчас возрадуются и разработчики на Фортране, потому как в нём такой поддержки не было (Cilk Plus только для С/С++) до последних дней.
Таким образом, в руках разработчиков появился мощный инструмент в использовании всех типов параллелизма в рамках одной спецификации, что не может не радовать. Но это далеко не всё что появилось. Не обошли вниманием и крайне популярную сейчас модель программирования на «гибридных» системах с ускорителями. И не важно, какой тип ускорителя – GPU ли это, или какой другой неведомый зверь — модель его использования с точки зрения разработчика теперь общая, и она так же часть new OpenMP. С помощью директивы target мы теперь можем провести вычисления на ускорители и получить результаты на хосте:

#pragma omp target map(to(b:count)) map(to(c,d)) map(from(a:count))
{
  #pragma omp parallel for
    for (i=0; i<count; i++)
      a[i] = b[i] * c + d;
}

Но это отдельная большая тема, о которой стоит поговорить. Кстати, OpenMP теперь всё чаще начинают сравнивать с другими стандартами, в частности OpenCL, OpenACC и другими. И интересное получается сравнение – с большим количеством «плюсов» для OpenMP. Но, вернёмся к теме.
Кроме всего вышесказанного, появились так же и другие «приятности», но я бы их отфильтровал как менее значимые, хотя и очень полезные для решения различного рода задач. Я говорю о новых средствах для обработки ошибок, о привязке потоков к конкретным ядрам, новых фичах для задач (tasking extensions), поддержке user-defined редукций и ряде других нововведений.

Полное описание можно классически найти на сайте OpenMP. Ну а один из первых компиляторов, который уже частично поддерживает ряд новых функций, здесь. Как всегда доступна и пробная 30-дневная версия. Так что welcome пробовать новые возможности OpenMP 4.0!
Tags:
Hubs:
+31
Comments 9
Comments Comments 9

Articles

Information

Website
www.intel.ru
Registered
Founded
Employees
5,001–10,000 employees
Location
США
Representative
Анастасия Казантаева