Параллельное программирование для многоядерных систем с помощью OpenMP

Источник: codingclub

История

Самым значимым событием 2005 года стало появление многоядерных процессоров. К тому времени классические одноядерные системы исчерпали свой потенциал, поскольку не давали нужной производительности за счет повышения частот, мало того разработчики архитектур столкнулись с проблемой нестабильности и повышенного тепловыделения. Все это волей-неволей способствовало появлению революционных многоядерных процессоров. Идея этих процессоров предельно проста: в одном процессоре интегрировано более одного ядра. Такой подход способствует появлению более производительных процессоров без повышения частот.

OpenMP

Тем не менее, на сегодняшний день не так уж и много приложений (особенно игр), которые полноценно используют многопроцессорные системы. Если Ваше приложение не использует несколько ядер, то соответственно и прироста быстродействия не будет. Именно здесь появляется на сцене OpenMP, технология, с помощью которой можно легко и быстро создавать мультипоточные приложения на C++ и соответственно существенно повысить производительности. Данная технология была разработана в 1997 году и изначально базировалась на языке Fortran. Сегодня она включена в язык C++ и доступна в Visual Studio 2005 (версия 2.0), а также на платформе XBOX 360.
Прежде чем приступать к коду, нужно активизировать реализованные в компиляторе средства OpenMP. Для этого в свойствах проекта C/C++ -> Language -> OpenMP ставим Yes(/openmp). Встретив параметр /openmp компилятор определяет символ _OPENMP, с помощью которого мы сможем определить используется ли данная технология в нашем приложении или нет. Так же, во избежание ошибок, в проект следует подключить хедер omp.h, который содержит нужные нам функции из vcomp.dll (или vcompd.dll в отладочной версии, которая содержит дополнительные сообщения об ошибках), манифесты и т.п.

"Helloworld"

OpenMP достаточно прост в использовании, он состоит из набора прагм и функций из omp.h. Прагмы - это указатели компилятору разбивать код на блоки, которые будут выполняться параллельно. Если openmp выключен, компилятор проигнорирует прагмы, а код останется вполне работоспособным. Все это дает возможность писать универсальный, переносимый код для реализации программ на различных архитектурах и системах. Спецификация OpenMP поддерживается такими поставщиками как Sun, Intel и IBM.
Все директивы начинаются с #pragma omp. Самая важная и распространенная директива - parallel. Она создает параллельный регион для следующего за ней блока. В качестве примера рассмотрим классический "Hello world":
Код:
int main()
{
 #pragma omp parallel
{
 printf("Hello world\n");
}
return 0;
}
При выполнении на двуядерной системе мы получим результат:

Код:
Hello world
Hello world

Рассмотрим более сложный пример, в котором используем конструкцию #pragma omp for, которая сообщает, что при выполнении цикла for в параллельном регионе итерации цикла должны быть распределены между потоками группы:
Код:
#pragma omp parallel
{
#pragma omp for
 for(int i < 0; i < num_Particles; i++) {
        Action(vec[i]);
}
}
Отметим очень важное замечание: если убрать из конструкции #pragma omp for, то каждый поток полностью выполнит цикл, таким образом будет выполнено много ненужной работы. В OpenMP также имеется возможность более краткой записи прагм #pragma omp parallel и #pragma omp for:
 
Код:
#pragma omp parallel for for(int i < 0; i < num_Particles; i++) {Action(vec[i]);
}

Барьеры и nowait

Барьеры являются одним из видов синхронизации. При обнаружении барьера поток останавливается и ждет пока все остальные потоки не окажутся на этом же барьере. Например, при выполнении цикла выше мы столкнулись с неявной барьерной синхронизацией, сами не заметив того. В конце цикла стоит барьер, который приостанавливает выполнение освободившихся потоков до тех пор, пока не освободятся другие потоки, обрабатывающие итерации этого же цикла. В некоторых случаях возникает потребность отключать синхронизацию, для этого существует оператор nowait, приведем пример, где это может быть выгодно:
Код:
#pragma omp parallel
{
#pragma omp for nowait
            for(int i = 0; i < 5000; i++) {
                        // …
            }
#pragma omp for
            for(int j = 0; j < 1000; j++) {
                        // …
            }
#pragma omp barrier
             some_func();
}В этом примере потоки, которые освободились после обработки первого цикла переходят к обработке второго цикла без ожидания остальных потоков. В некоторых случаях это может повысить производительность, поскольку уменьшается время простоя потоков. Возможна и обратная ситуация, когда нам нужно организовать барьер. В примере выше функция some_func() не выполнится до тех пор, пока все потоки не выполнят предыдущие задачи (это нужно, например, при обновлении буфера кадров перед выводом его содержимого на экран).

Ограничение на циклы

В OpenMP существует ряд ограничений на циклы, которые нужно распараллелить:
1.Переменная цикла должна иметь тип integer. Беззнаковые целые числа, такие как DWORD, работать не будут.
2.Цикл должен являться базовым блоком и не может использовать goto и break (за исключением оператора exit, который завершает все приложение).
3.Инкрементная часть цикла for должно являться либо целочисленным сложением, либо целочисленным вычитанием и должно практически совпадать со значением инварианта цикла.
4.Если используется операция сравнения < или <=, переменная цикла должна увеличиваться при каждой итерации, а при использовании операции > или >= переменная цикла должна уменьшаться.

Критические разделы

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

Код:
#pragma omp critical(some_value)
{
      …
}

Конструкция Sections

OpenMP применим не только для циклов, но и для функций. Конструкция sections используется когда необходимо выполнять блоки кода в отдельных потоках. Рассмотрим простейший пример (сортировка):
Код:
#pragma omp parallel sections
{
            #pragma omp section
            sort(array, …);
            #pragma omp section
            sort(array, …);
}
В данном примере первая директива #pragma создает параллельный регион секций, а последующие секции определяются директивой #pragma omp section. Каждой секции в параллельном регионе ставится в соответствие один поток из группы потоков, и все секции выполняются одновременно. Заметим, что каждая из функций sort выполняется полностью независимо друг от друга, это очень важно, поскольку приводит к тривиальным ошибкам, которые очень тяжело отловить. Рассмотрим пример ошибочного кода:
Код:
int n, m, q;
#pragma omp parallel sections
{
            #pragma omp section
            n = partition(array, lo, hi);
            #pragma omp section
            m = partition(array, lo, n-1); // n возможно не определен!!
            #pragma omp section
            q = partition(array, n+1, hi);  // n возможно не определен!!
}При компиляции приложения без параметра /openmp будет сгенерирована корректная последовательная версия. Одно из преимуществ OpenMP в том, что эта технология совместима с компиляторами, не поддерживающими OpenMP.
Выводы
OpenMP быстрая, простая и легкая технология с помощью которой можно использовать потенциал многоядерных систем. Всего за несколько минут обычный код можно переделать в оптимизированный, при этом остается совместимость с компиляторами, которые не поддерживают данную технологию. Возможное применение в игровых движках: particle system, collision detection, fractals, поиск путей и т. д. К сожалению, в данной статье мы коротко рассмотрели лишь малую часть возможностей данной технологии, но более подробную спецификацию вы можете найти на сайте http://www.openmp.org.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=22962