Вы находитесь на странице: 1из 13

САНКТ-ПЕТЕРБУРГСКИЙ НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ

УНИВЕРСИТЕТ ИТМО

Дисциплина: Архитектура ЭВМ

Отчет
по домашней работе № 5

«OpenMP»

Выполнил(а): Багрицевич Степан Александрович

Номер ИСУ: 313068

студ. гр. M3138

Санкт-Петербург

2020
Цель работы: знакомство со стандартом распараллеливания команд

OpenMP.

Инструментарий и требования к работе: рекомендуется использовать C,


C++. Возможно использовать Python и Java.

Теоретическая часть

OpenMP — это библиотека для параллельного программирования


вычислительных систем с общей памятью. Официально поддерживается
Си, С++ и Фортран, однако можно найти реализации для некоторых других
языков, например Паскаль и Java. Преимущество использования OpenMP в
том, что программисту не нужно писать код, синхронизирующий несколько
потоков вручную.

Чтобы использовать OpenMP необходимо подключить заголовочный


файл "omp.h", а также при запуске программы добавить опцию сборки -
fopenmp (для компилятора gcc).

Конструкции OpenMP являются директивами компилятора, по этой


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

#pragma omp (конструкция) [условия]

Основная конструкция в OpenMP – параллельная область, которая


задается директивой

#pragma omp parallel

Границы параллельной области задаются фигурными скобками.


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

Теперь рассмотрим доступные условия выполнения параллельной


области:

• shared(x, y, …). Условие указывает на то, что все перечисленные


переменные будут доступны каждому потоку, то есть все потоки
будут иметь доступ к одной и той же области памяти;
• private(x, y, …). Условие указывает на то, что для каждого потока
будут созданы копии перечисленных переменных;
• firstprivate(x, y, …). Условие аналогично предыдущему, но с одним
отличием. Все копии перечисленных переменных будут
инициализированы значениями, которые имели указанные
переменные до входа в параллельную область;
• lastprivate(x, y, …). Условие похоже на private(x, y, …), но с одним
отличием. Перечисленные переменные сохранят значение, которое
они получили в конце параллельного участка кода;
• reduction(x, y, …). Это условие разберём чуть позже;
• if(выражение). Условие позволяет задать выражение, при истинности
которого параллельное выполнение области необходимо;
• default(shared | private | none). Это условие определяет тип видимости
переменных внутри параллельной области по умолчанию.

Основная ошибка, которая может возникнуть при работе с потоками


– это ошибки соревнования (race condition). Они возникают при
неаккуратном использовании разделяемых (shared) переменных, потоки
выполняются параллельно, следовательно последовательность доступа к
разделяемым переменным может быть различна от одного запуска
программы к другому. Для того чтобы это предотвратить надо
использовать синхронизацию потоков, которая достигается благодаря
критическим секциям, atomic или barrier.

Рассмотрим пример гонки потоков, код:

int x = 1;

#pragma omp parallel

cout << ++x << '\n';

Код создает переменную x, которая общая для всех потоков. Каждый


поток увеличивает значение переменной, а затем выводит результат на
экран. При запуске данного кода мы получаем разные результаты, пример
первого запуска на рисунке 1, пример второго запуска на рисунке 2.

Рисунок № 1 – Гонка потоков, результат первого запуска

Рисунок № 2 – Гонка потоков, результат второго запуска

Теперь воспользуемся критической секцией (директива critical):

int x = 1;

#pragma omp parallel


{

#pragma omp critical

cout << ++x << '\n';

В критической секции в один момент времени может находиться


только один поток, остальные ожидают ее освобождения, таким образом мы
получаем последовательный вывод (рисунок 3).

Рисунок № 3 – Результат после использования критической секции

Для ряда операций более эффективно использовать директиву atomic,


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

Теперь рассмотрим директиву for, которая позволяет распараллелить


цикл. У неё такие же условия выполнения, как и у параллельной области,
но некоторые условия мы не разобрали, рассматривая параллельную
область:

• reduction(x, y, …). Условие гарантирует безопасное выполнение


операций редукции, поддерживаемые операции и функции: +, -, *, &,
^, |, &&, ||, min, max;
• schedule(static | dynamic | guided). Условие определяет, каким образом
итерации цикла будут распределяться между потоками. Разберём все
типы:
• static – итерации равномерно распределяются по потокам;
• dynamic – работа распределяется пакетами заданного размера
(по умолчанию размер равен 1) между потоками. Как только
какой-либо из потоков заканчивает обработку своей порции
данных, он захватывает следующую;
• guided – данный тип распределения работы аналогичен
предыдущему, но размер блока изменяется динамически в
зависимости от того, сколько необработанных итераций
осталось.

Так же стоит учитывать, что распараллелить цикл можно только когда


его итерации не зависят от друг друга.

Теперь рассмотрим параллельный алгоритм вычисления префиксной


суммы. Очевидно, что стандартный алгоритм, работающий за линию, нам
не подходит, так как в нём итерации зависят от друг друга. Воспользуемся
чем-то похожим на алгоритм построения sparce table. Мы будем перебирать
длину шага step, которая является степенью двойки (т.е. перебираем
степень двойки). И прибавлять каждому i-ому элементу массива pref (i –
2^step)-ый элемент pref. Перебирать step мы будем пока 2^step меньше
длины нашего изначального массива. Таким образом мы как бы будем
«проталкивать» каждый элемент вперёд.

Практическая часть

Весь код функции, которая параллельно вычисляет префиксную


сумму для массива a размера n, так же на вход подается количество потоков:
vector<int> prefSumParallelAlgorithm(const vector<int> &a, const int
threadsCnt) {

int n = (int)a.size();

vector<int> prevPrefSum = a, curPrefSum = a;

if (threadsCnt) {

omp_set_num_threads(threadsCnt);

for (int step = 0; (1 << step) < n; step++) {

#pragma omp for schedule(static)

for (int i = 0; i < n - (1 << step); i++) {

curPrefSum[i + (1 << step)] += prevPrefSum[i];

prevPrefSum = curPrefSum;

return curPrefSum;

Теперь разберём код по частям. Сначала мы создаем два вектора


prevPrefSum (предыдущая префиксная сумма), curPrefSum (текущая
префиксная сумма). Мы выполняем алгоритм изменяя длину шага, поэтому
prevPrefSum – результат прошлого шага. По условию задачи если введённое
количество потоков равно 0, то количество потоков равно значению по
умолчанию, иначе устанавливаем введённое количество потоков:

if (threadsCnt) {

omp_set_num_threads(threadsCnt);

Далее в цикле реализован сам алгоритм, заметим, что внешний цикл


(где мы перебираем длину шага) нельзя распараллелить, так как в нём
итерации зависят друг от друга. Теперь во внутреннем цикле мы
перебираем элемент, который мы будем проталкивать вперёд. Внутренний
цикл мы можем распараллелить, так как в нём итерации независимы,
каждая итерация зависит только от значений, которые были получены на
шаге другой длины. В конце внешнего цикла мы копируем curPrefSum в
prevPrefSum. Результат находится в curPrefSum (prevPrefSum тоже
подходит).

Диаграмма a, b указана на рисунке 4. Диаграмма с указана на рисунке


5.

Рисунок № 4 – Диаграмма a, b
Рисунок № 5 – Диаграмма c

Листинг

Язык – C++14. Компилятор – GNU gcc 10.2.0.

main.cpp
#include <iostream>

#include <vector>

#include <omp.h>

#include <chrono>

#include <sstream>

#include <fstream>

using namespace std;


vector<int> prefSum(const vector<int> &a) {

int n = (int)a.size();

vector<int> prevPrefSum = a, curPrefSum = a;

for (int step = 0; (1 << step) < n; step++) {

for (int i = 0; i < n - (1 << step); i++) {

curPrefSum[i + (1 << step)] += prevPrefSum[i];

prevPrefSum = curPrefSum;

return curPrefSum;

vector<int> prefSumParallelAlgorithm(const vector<int> &a, const int


threadsCnt) {

int n = (int)a.size();

vector<int> prevPrefSum = a, curPrefSum = a;

if (threadsCnt) {

omp_set_num_threads(threadsCnt);

for (int step = 0; (1 << step) < n; step++) {

#pragma omp for schedule(static)

for (int i = 0; i < n - (1 << step); i++) {


curPrefSum[i + (1 << step)] += prevPrefSum[i];

prevPrefSum = curPrefSum;

return curPrefSum;

int main(int argc, char *argv[]) {

const int firstIndex = 2;

const int argCnt = argc - firstIndex;

if (argCnt < 2) {

cout << "Error: Not enough arguments";

return 0;

int threadsCnt;

stringstream convert(argv[firstIndex]);

if (!(convert >> threadsCnt)) {

cout << "Error: First argument isn't integer";

return 0;

ifstream fin(argv[firstIndex + 1]);


if (!fin.is_open()) {

cout << "Error: Input file not found";

return 0;

int n;

if (!(fin >> n)) {

cout << "Error: N isn't integer";

return 0;

vector<int> a(n);

for (int i = 0; i < n; i++) {

if (!(fin >> a[i])) {

cout << "Error: Element " + to_string(i + 1) + " isn't


integer";

return 0;

auto sequentialAlgorithmStart = chrono::steady_clock::now();

vector<int> prefSequentialAlgorithm = prefSum(a);

auto sequentialAlgorithmEnd = chrono::steady_clock::now();

auto parallelAlgorithmStart = chrono::steady_clock::now();

vector<int> prefParallelAlgorithm = prefSumParallelAlgorithm(a,


threadsCnt);

auto parallelAlgorithmEnd = chrono::steady_clock::now();


if (argCnt > 2) {

ofstream fout(argv[firstIndex + 2]);

if (fout.fail()) {

cout << "Error: Fail with output file";

return 0;

fout << n << '\n';

for (const int &x : prefParallelAlgorithm) {

fout << x << ' ';

fout << '\n';

} else {

cout << n << '\n';

for (const int &x : prefParallelAlgorithm) {

cout << x << ' ';

cout << '\n';

cout << "Time: " <<


chrono::duration_cast<chrono::milliseconds>(sequentialAlgorithmEnd -
sequentialAlgorithmStart).count() << "ms\n";

cout << "Time (" << threadsCnt << " thread" << (threadsCnt > 1 ? "s" :
"") << "): " <<
chrono::duration_cast<chrono::milliseconds>(parallelAlgorithmEnd -
parallelAlgorithmStart).count() << "ms\n";

return 0;

Вам также может понравиться