Паралельне програмування з використанням технології OpenMP

Основні компоненти OpenMP ред.

З точки зору прикладного програміста до складу середовища OpenMP входять три основних компоненти:

• набір директив транслятору;

• набір функцій бібліотеки;

• змінні оточення;

Директиви слугують основним засобом вираження паралелізму в OpenMP.

Вони представляють собою спеціальним чином оформлені коментарі (Fortran) або директиви компілятору – «прагми» (C/C++). Це дозволяє ігнорувати такі директиви звичайними трансляторами, які не підтримують OpenMP. В той же час в паралельному варіанті програми можуть з’явитися помилки, які стали результатом некоректного проведеного розпаралелювання.

Функцій бібліотеки дозволяють керувати різноманітними характеристиками в процесі виконання OpenMP-програми, наприклад, числом потоків і т. ін.

За допомогою змінних оточення користувач має можливість керувати налаштуваннями середовища виконання, наприклад, встановлювати характерні параметри розпаралелювання циклів. Використання змінних оточення дозволяє впливати на поведінку паралельної програми без перекомпіляції.[1]

Модель виконання OpenMP-програми ред.

В OpenMP використовується модель паралельного виконання «розгалуження-злиття» («ветвление-слияние»). Головний (master-thread) потік породжує групу потоків по мірі необхідності. Паралелізм додається інкрементально (покроково).

Програма OpenMP починається як єдиний потік виконання, що називається початковим потоком. Коли потік зустрічає паралельну конструкцію, він створює нову групу потоків, що складається із себе та ще декількох додаткових потоків, та стає головним в групі. Всі члени нової групи (включаючи головний) виконують вод всередині паралельної конструкції. В кінці паралельної конструкції є неявний бар’єр. Після паралельної конструкції виконання користувацького коду продовжує лише головний потік. В паралельний регіон можуть бути вкладені інші паралельні регіони, в яких кожен потік початкового регіону стає основним для своєї групи потоків. Вкладені регіони можуть в свою чергу включати регіони більш глибокого рівня вкладеності (рисунок 1.1).[2]

 
Рисунок 1.1 – Модель виконання OpenMP-програми

Число потоків в групі, що виконуються паралельно, можна контролювати декількома способами. Один із них – використання змінної оточення OMP_NUM_THREADS. Інший спосіб – виклик процедури omp_set_num_threads(), ще один спосіб – використання виразу num_threads в поєднанні з директивою parallel. Детальніше про це буде йти мова далі.[3]

Модель пам’яті OpenMP-програми. Класи змінних ред.

Змінні в OpenMP-програмі у загальному випадку діляться на два класи: спільні (shared) та індивідуальні (private):

  • Індивідуальні змінні відповідають деякому потоку та можуть зчитуватися або записуватися тільки ним.
  • Спільні змінні доступні для зчитування або запису декількома потоками однієї групи.

Варто зауважити, що у випадку, якщо зчитування і запис або повторний запис спільної змінної відбувається без синхронізації, то результуюче значення змінної вважається невизначеним. Модель представлено пам’яті OpenMP-програми представлено на рисунку 2.1

 
Рисунок 2.1 - Модель пам’яті OpenMP-програми

В моделі програмування із спільною пам’яттю в OpenMP більшість змінних за замочуванням вважаються shared (глобальними, спільними). Глобальні змінні спільно використовуються усіма потоками (shared).[4]

  • Фортран: COMMON блоки, SAVE змінні, MODULE змінні;
  • Сі: file scope, static;
  • Динамічно виділена пам’ять (ALOCATE, malloc, new).

Не всі змінні є спільними:

  • Стекові змінні в підпрограмах (функціях), що викликаються з паралельної частини програми, є приватними;
  • Змінні, що оголошені всередині блоку операторів паралельної частини програми є приватними;
  • Лічильники циклів, витки яких розподіляються між потоками (нитями) за допомогою конструкцій FORта PARALLELFOR.

На рисунку 3.2 представлено випадок належності змінних до спільної чи ні пам’яті:

 
Рисунок 3.2 – Приклад розташування змінних в пам’яті

Як видно з рисунку 3.2 змінні Array1, Array2 та count є глобальними відносно паралельної частини програмного коду, а змінні TempArray, iam є локальними змінними, причому на кожному потоці їхнє значення може відрізнятися

.Можна змінити клас змінної з допомогою конструкцій:

  • SHARED (список змінних)
  • PRIVATE (список змінних)
  • FIRSTPRIVATE (список змінних)
  • LASTPRIVATE (список змінних)
  • THREADPRIVATE (список змінних)
  • DEFAULT (PRIVATE | SHARED | NONE)

Конструкція «private(var)» створює локальну копію змінної «var» на кожному з потоків (нитей):

  • Значення змінної не ініціалізовано;
  • Приватна копія не пов’язана з оригіналом змінної;
  • В OpenMP 2.5 значення змінної «var» не визначено після завершення паралельної конструкції.

Приклад коду:

#pragma omp parallel for private (i,j,sum)
     for (i=0; i< m; i++)
 {
     sum = 0.0;
       for (j=0; j< n; j++)
         sum +=b[i][j]*c[j];
           a[i] = sum;
 }

Конструкція «firstprivate(var)» є спеціальним випадком «private(var)»:

• Ініціалізує кожну приватну копію відповідним значенням з головного потоку.

Приклад коду:

BOOL FirstTime=TRUE;
 #pragma omp parallel for firstprivate(FirstTime)
     for (row=0; row<height; row++)
{
       if (FirstTime == TRUE) { FirstTime = FALSE; FirstWork (row); 
}
AnotherWork (row);
}

Конструкція «lastprivate(var)» передає значення приватної змінної, що обчислена на останній ітерації, в глобальну змінну.

Приклад коду:

int i;
#pragma omp parallel
{
  #pragma omp for lastprivate(i)
     for (i=0; i<n-1; i++)
         a[i] = b[i] + b[i+1];
}
[i]=b[i]; /*i == n-1*/

Конструкція «threadprivate(var)»

(рисунок 4.3) відрізняється від застосування конструкції «private(var)»:

  • З PRIVATE глобальні змінні маскуються;
  • THREADPRIVATE зберігає глобальну область видимості всередині кожного потоку.

Конструкція:

  1. pragma omp threadprivate (Var)
 
Рисунок 4.3 – Конструкція THREADPRIVATE

Якщо кількість потоків не змінилась, то кожен потік отримає значення, що обчислене в попередній паралельній частині програми.

Конструкція DEFAULT змінює клас змінної за замовчуванням:

  • DEFAULT (SHARED) – діє за замовчуванням;
  • DEFAULT (PRIVATE) – є тільки в Fortran;
  • DEFAULT (NONE) – потребує визначити клас для кожної змінної.

Компіляція OpenMP-програми ред.

Для компіляції OpenMP-програми потрібно мати відповідні програмні засоби. Такі компілятори представлені в таблиці 1

Виробник Компілятор
GNU gcc
IBM XL C/C++ / Fortran
Sun Microsystems C/C++ / Fortran
Intel C/C++ / Fortran
Portland Group C/C++ / Fortran
Microsoft Visual Studio 2008 C++

Є так звана умовна компіляція OpenMP-програми:

#include <stdio.h>
int main()
{
   #ifdef _OPENMP
         printf("Compiled by an OpenMP-compliant implementation.\n");
   #endif
return 0;
}

В значенні змінної _OPENMP зашифрований рік та місяць (yyyymm) версії стандарту OpenMP, яку підтримує компілятор. І, якщо _OPENMP була визначена, то програмне середовище відкомпілює даний код з використанням OpenMP засобів, а якщо ні - OpenMP засоби використані не будуть, відповідно не екран консольної строки нічого виведено не буде.

Примітки ред.

  1. Корнеев, В.В. (1999). Параллельные вычислительные системы.
  2. Гома, Х (2002). UML. Проектирование систем реального времени, па- раллельных и распределенных приложений.
  3. Немнюгин, Стесик (2002). Параллельное программирование для многопроцессорных вычислительных систем. СПб.
  4. Антонов, А.С. (2009). Параллельное программирование с использованием технологии OpenMP.

Література ред.