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. |