40
Параллельные алгоритмы обработки данных Андрей Карпов, Антон Миронов Декабрь 2008

Параллельные алгоритмы обработки данных

Embed Size (px)

Citation preview

Page 1: Параллельные алгоритмы обработки данных

Параллельные алгоритмы обработки данных

Андрей Карпов, Антон Миронов

Декабрь 2008

Page 2: Параллельные алгоритмы обработки данных

Аннотация Статья посвящена вопросам использования параллельных алгоритмов для создания современных

эффективных программных решений. Актуальность данной тематики обусловлена снижением темпов роста

тактовой частоты микропроцессоров и возрастанием внимания к использованию всех возможностей

многоядерных и многопроцессорных систем. В работе рассмотрен ряд базовых параллельных алгоритмов,

таких как умножение матриц, параллельная сортировка Бэтчера, метод Гаусса решения систем линейных

алгебраических уравнений и так далее. Приведена реализация этих алгоритмов c использованием языка

программирования Си++.

Page 3: Параллельные алгоритмы обработки данных

Введение В ближайшее время под эффективным использованием аппаратных средств компьютера будут пониматься

применение параллельных алгоритмов. Это связано с замедлением темпов роста тактовой частоты

микропроцессоров и быстрым распространением многоядерных микропроцессоров.

Создание эффективных программных решений для параллельных систем складывается из трех основных

компонентов: параллельных алгоритмов, средств реализации параллельности и систем отладки.

Под средствами реализации параллельности понимаются языки программирования или библиотеки,

обеспечивающие инфраструктуру параллельных программ. Таких система достаточно много. К ним можно

отнести: Occam, MPI, HPF, OpenMP, DVM, OpenTS, Boost.Thread, Posix Threads. Сюда также можно отнести

библиотеку Integrated Performance Primitives компании Intel.

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

последовательной программы, то системы отладки и профилирования являются важнейшей частью в процессе

разработки таких систем. В области отладчиков хочется обратить внимание на системы TotalView и PGDBG.

Среди средств профилирования можно назвать такие инструменты как Nupshot, Pablo, Vampir. Существуют

системы статической верификации, например VivaMP. Последней инструмент, на который хочется обратить

особое внимание, является средства анализа многопоточности Threading Analysis Tools.

Page 4: Параллельные алгоритмы обработки данных

Но самый важный компонент, без которого все другие средства не смогут сделать программу параллельной,

это параллельный алгоритм. Именно этому компоненту и будет посвящена эта статья.

В статье будут рассмотрены несколько базовых параллельных алгоритмов, имеющих практическое значение

во многих областях, в том числе в экономике, в численном моделировании и в задачах реального времени.

Для некоторых алгоритмов приведены оценки времени выполнения последовательной и параллельной

версий алгоритма и выведены коэффициенты ускорения. Приведен пример реализации описанных

алгоритмов c использованием языка программирования Си++ в среде Windows.

Page 5: Параллельные алгоритмы обработки данных

1. Параллельная сортировка Бэтчера Алгоритм сортировки Бэтчера не является наиболее эффективным алгоритмом сортировки, однако он

обладает одним важным компенсирующим качеством: все сравнения и/или обмены, определяемые данной

итерацией алгоритма можно выполнять одновременно. С такими параллельными операциями сортировка

осуществляется за . Например, 1024 элемента можно рассортировать методом Бэтчера

всего за 55 параллельных шагов. Схема сортировки Бэтчера несколько напоминает сортировку Шелла, но

сравнения выполняются по-новому, а потому цепочки операций обмена записей не возникает. Поскольку в

алгоритме Бэтчера, по существу, происходит слияние пар рассортированных подпоследовательностей, его

можно назвать обменной сортировкой со слиянием.

Алгоритм Бэтчера (обменная сортировка со слиянием). Записи перекомпоновываются в пределах того

же пространства в памяти. После завершения сортировки их ключи будут упорядочены: .

Предполагается, что .

1)[начальная установка .] Установить , где — наименьшее целое число, такое, что .

(Шаги 2-5 будут выполняться с .)

2)[начальная установка .] Установить .

3)[цикл по .] Для всех , таких, что и , выполнять шаг 4.

Page 6: Параллельные алгоритмы обработки данных

Затем перейти к шагу 5. (Здесь через обозначена операция "поразрядное логическое И" над

представлениями целых чисел и ; все биты результата равны 0, кроме тех битов, для которых в

соответствующих разрядах и находятся 1. Так . К этому моменту —

нечётное кратное (т.е. частное от деления на нечётно), а — степень двойки, так что .

Отсюда следует, что шаг 4 можно выполнять при всех нужных значениях в любом порядке или даже

одновременно.)

4)[Сравнение/обмен .] Если , поменять местами записи .

5)[Цикл по .] Если , установить и возвратиться к шагу 3.

6)[Цикл по ] (К этому моменту перестановка будет -упорядочена.) Установить .

Если , возвратиться к шагу 2.

Page 7: Параллельные алгоритмы обработки данных

//////////////////////////////////////// // Параллельная сортировка Бэтчера #include <stdio.h> #include <stdlib.h> #define N 16 int Arr[N]; void FillArray() { for (unsigned i = 0; i < N; i++) Arr[i] = rand() % 10; } void PrintArray() { for (unsigned i = 0; i < N; i++) printf("%d ", Arr[i]); printf("\n"); } void BatcherSort() { unsigned p = N; while (p > 0) { unsigned q = N, r = 0, d = p; bool b; do { unsigned nTo = N - d; for (unsigned i = 0; i < nTo; i++) if ((i & p) == r) { if (Arr[i] > Arr[i + d]) { int temp = Arr[i];

Page 8: Параллельные алгоритмы обработки данных

Arr[i] = Arr[i + d]; Arr[i + d] = temp; } } b = q != p; if (b) { d = q - p; q >>= 1; r = p; } } while (b); p >>= 1; } } int main(int argc, char* argv[]) { FillArray(); PrintArray(); BatcherSort(); PrintArray(); return 0; }

Page 9: Параллельные алгоритмы обработки данных

2. Вычисление корня алгебраического или трансцендентного уравнения

Пусть дано уравнение . Требуется на отрезке найти корень при условии . Для

построения алгоритма ветви используется метод хорд, заключающийся в следующем (рисунок N1).

Кривая заменяется хордой , определяется

Рисунок 1. Метод хорд.

точка , после чего проводится хорда определяется и т. д., пока .

Для реализации задачи на процессоров, т.е. для построения параллельного алгоритма из ветвей,

отрезок разбивается на подотрезков на границах которых вычисляется . Это определяет

подотрезок длиной , содержащий корень . Точки и — соседние, в них значения функции

имеют разные знаки (рисунок N1).

Page 10: Параллельные алгоритмы обработки данных

После отрезка рассматривается отрезок , где

(1)

Отрезок разбивается на частей, повторяется описанная процедура, получается отрезок и т. д.,

пока длина отрезка не станет меньше заданного значения. Параллельный счет оправдан, когда объём

вычислений значения намного превосходит объём вычислений границ отрезка по (1).

Аналогичный результат получается и при использовании метода Ньютона.

//////////////////////////////////////// // Параллельное вычисление корня уравнения #include <windows.h> #include <process.h> #include <stdio.h> #include <math.h> #define NumberOfProcessors 3 float Func(float arg) { return arg * (arg - 5) + 6; } template <typename t> inline int sgn(t arg) { return arg < 0 ? -1 : (arg > 0 ? 1 : 0); } float x1 = 2.5; float x2 = 10; float e = 1;

Page 11: Параллельные алгоритмы обработки данных

float M = 5; #define EPSILON 1e-4 enum Commands { Continue = 0, Stop = 1 }; int Action = Continue; HANDLE hEventStart[NumberOfProcessors]; HANDLE hEventFinish[NumberOfProcessors]; float Values[NumberOfProcessors]; unsigned __stdcall ThreadFunction(void *pData) { unsigned Branch = (unsigned)pData; for (;;) { WaitForSingleObject(hEventStart[Branch], INFINITE); if (Action == Stop) return 0; Values[Branch] = Func(x1 + Branch * (x2 - x1) / (float)(NumberOfProcessors - 1)); SetEvent(hEventFinish[Branch]); } return 0; } int main(int argc, char* argv[]) { UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); HANDLE hThreads[NumberOfProcessors]; unsigned i, tid; float Result = 0.0f; for (i = 0; i < NumberOfProcessors; i++) { hEventStart[i] = CreateEvent(NULL, FALSE, TRUE, NULL); hEventFinish[i] = CreateEvent(NULL, FALSE,

Page 12: Параллельные алгоритмы обработки данных

FALSE, NULL); } for (i = 0; i < NumberOfProcessors; i++) hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, (void *)i, 0, &tid); for (;;) { WaitForMultipleObjects(NumberOfProcessors, hEventFinish, TRUE, INFINITE); int Sign = sgn(Values[0]); if (Sign == 0) { Result = x1; Action = Stop; } if (Action != Stop) for (i = 1; i < NumberOfProcessors; i++) { int NewSign = sgn(Values[i]); if (NewSign == 0) { Result = x1 + i * (x2 - x1) / (float)(NumberOfProcessors - 1); Action = Stop; break; } if (NewSign != Sign) { float a = x1 + (i - 1) * (x2 - x1) / (float)(NumberOfProcessors - 1); float b = x1 + i * (x2 - x1) /

Page 13: Параллельные алгоритмы обработки данных

(float)(NumberOfProcessors - 1); x1 = a + (Values[i - 1] * (b - a)) / (Values[i] - Values[i - 1]); x2 = b - Values[i] / M; e = b - a; break; } } if (e < EPSILON) { Result = (x2 + x1) / 2; Action = Stop; } for (i = 0; i < NumberOfProcessors; i++) SetEvent(hEventStart[i]); if (Action == Stop) break; } printf("%7.4f", Result); for (i = 0; i < NumberOfProcessors; i++) { CloseHandle(hThreads[i]); CloseHandle(hEventStart[i]); CloseHandle(hEventFinish[i]); } return 0; }

Page 14: Параллельные алгоритмы обработки данных

3. Решение системы линейных алгебраических уравнений методом простой

итерации

Система уравнений размерности записывается в виде , где — матрицы;

и — векторы; — единичная матрица.

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

приближения . Процесс завершается, если . Последовательность в методе простой

итерации сходится, если для матрицы выполняется одно из неравенств

Применим к матрице и векторам и распределение горизонтальными полосами. Для нахождения

в соответствующей ветви необходимы строка матрицы , вектор и значение . Поэтому можно

обеспечить каждую ветвь всеми необходимыми данными для автономной работы. Отсюда

Page 15: Параллельные алгоритмы обработки данных

где — число строк матрицы и компонентов векторов и , обрабатываемых отдельной ветвью, —

время обмена данными, — время выполнения сложения, — время выполнения умножения.

#include <windows.h> #include <process.h> #include <stdio.h> #include <math.h> #define N 4 #define NumberOfProcessors 2 #define EPSILON 1e-5 float Matrix[N][N + 1] = { { 1.0f, 0.1f, 0.1f, 0.1f, 1.0f }, { 0.1f, 1.2f, 0.1f, 0.1f, 1.0f }, { 0.2f, 0.1f, 1.0f, 0.1f, 1.0f }, { 0.2f, 0.2f, 0.2f, 1.0f, 1.0f } }; float NewX[N]; float CurX[N] = { 1.0, 1.0, 1.0, 1.0 }; void InitializeMatrix() { printf("Source Matrix\n"); for (unsigned i = 0; i < N; i++) { for (unsigned j = 0; j <= N; j++) { // calculate (E - A) if (i == j) Matrix[i][j] = 1.0f - Matrix[i][j]; else if (j < N)

Page 16: Параллельные алгоритмы обработки данных

Matrix[i][j] = -Matrix[i][j]; printf("%7.4f ", Matrix[i][j]); } printf("\n"); } } enum Commands { Continue, Stop }; struct IterState { HANDLE Starters[NumberOfProcessors]; HANDLE evIterationFinished[NumberOfProcessors]; }; IterState State; unsigned Command = Continue; unsigned __stdcall ThreadFunction(void *pData) { unsigned Branch = (unsigned)pData; unsigned nFrom = Branch * N / NumberOfProcessors; unsigned nTo = (Branch + 1) * N / NumberOfProcessors; unsigned i; for (;;) { WaitForSingleObject(State.Starters[Branch], INFINITE); if (Command == Stop) return 0; for (i = nFrom; i < nTo; i++) { NewX[i] = Matrix[i][0] * CurX[0]; for (unsigned j = 1; j < N; j++) NewX[i] += Matrix[i][j] * CurX[j]; NewX[i] += Matrix[i][N]; } // synchronization

Page 17: Параллельные алгоритмы обработки данных

SetEvent(State.evIterationFinished[Branch]); if (Branch == 0) { WaitForMultipleObjects(NumberOfProcessors, State.evIterationFinished, TRUE, INFINITE); float d = -1; for (i = 0; i < N; i++) if (fabs(NewX[i] - CurX[i]) > d) d = (float)fabs(NewX[i] - CurX[i]); if (d < EPSILON) Command = Stop; memcpy(CurX, NewX, sizeof(NewX)); for (i = 0; i < NumberOfProcessors; i++) SetEvent(State.Starters[i]); } } } int main(int argc, char* argv[]) { UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); InitializeMatrix(); HANDLE hThreads[NumberOfProcessors]; unsigned i, tid; for (i = 0; i < NumberOfProcessors; i++) { State.Starters[i] = CreateEvent(NULL, FALSE, TRUE, NULL); State.evIterationFinished[i] = CreateEvent(NULL, FALSE, FALSE, NULL); }

Page 18: Параллельные алгоритмы обработки данных

for (i = 0; i < NumberOfProcessors; i++) hThreads[i] = (HANDLE)_beginthreadex( NULL, 0, ThreadFunction, (void *)i, 0, &tid); WaitForMultipleObjects(NumberOfProcessors, hThreads, TRUE, INFINITE); for (i = 0; i < NumberOfProcessors; i++) { CloseHandle(State.Starters[i]); CloseHandle(State.evIterationFinished[i]); CloseHandle(hThreads[i]); } for (i = 0; i < N; i++) printf("x%d = %7.4f\n", i, NewX[i]); return 0; }

Page 19: Параллельные алгоритмы обработки данных

4. Решение системы линейных уравнений методом Гаусса Метод Гаусса основан на последовательном исключении неизвестных. Пусть дана система линейных

алгебраических уравнений

Вычтем из второго уравнения системы первое, умноженное на такое число, чтобы уничтожился коэффициент

при . Затем таким же образом вычтем первое уравнение из третьего, четвёртого и т.д. Тогда исключатся все

коэффициенты первого столбца, лежащие ниже главной диагонали. Затем при помощи второго уравнения

исключим из третьего, четвёртого и т.д. уравнений коэффициенты второго столбца. Последовательно

продолжая этот процесс, исключим из матрицы все коэффициенты, лежащие ниже главной диагонали.

Запишем общие формулы процесса. Пусть проведено исключение коэффициентов из столбца. Тогда остались

такие уравнения с ненулевыми коэффициентами ниже главной диагонали:

Умножим k-ю строку на число

Page 20: Параллельные алгоритмы обработки данных

и вычтем из m-й строки. Первый элемент этой строки обратится в нуль, а остальные изменятся по формулам

Производя вычисления по этим формулам при всех указанных индексах, исключим элементы k-го столбца.

Будем называть такое исключение циклом процесса. Выполнение всех циклов называется прямым ходом

исключения. После выполнения прямого хода получим треугольную систему

с матрицей

Далее треугольная система решается обратным ходом по формулам

Page 21: Параллельные алгоритмы обработки данных

Оценим время выполнения описанного алгоритма. Не нарушая общности, будем предполагать, что первый

элемент первой строки не равен нулю и элементы на главной диагонали в ходе расчёта также не обращаются

в нуль.

Пусть , , , — время, затрачиваемое на сложение, вычитание, умножение и деление двух чисел

соответственно. Количество уравнений в системе обозначим .

Рассмотрим сначала прямой ход метода Гаусса. Первая строка обстается без изменений, поэтому e

исключение выполняется за время:

Тогда время, затраченное на выполнение прямого хода, будет равно:

Рассмотрим обратный ход метода Гаусса. Определение требует времени:

Соответственно, время выполнения обратного хода равно:

Page 22: Параллельные алгоритмы обработки данных

Итого, общее время

Рассмотрим параллельный алгоритм, реализующий метод Гаусса. Пусть имеется процессоров, причём для

простоты, , где — целое.

Процесс деления строк на коэффициенты перед независим и не требует информации от других ветвей.

Использование распределений горизонтальными или вертикальными полосами для данного метода ведет к

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

вычисления таким образом, чтобы процессор обсчитывал строки с номерами

(распределение циклическими горизонтальными полосами с шириной полосы равной единице).

Page 23: Параллельные алгоритмы обработки данных

Рисунок 2. Схема параллельного метода Гаусса.

Так как и , то

,

Таким образом, если не учитывать простой процессоров вызванный (вообще говоря) неодинаковым

количеством обсчитываемых строк и неравномерностью самих вычислений, параллельный алгоритм работает

в K раз быстрее последовательного.

//////////////////////////////////////// // Приведение расширенной матрицы к диагональному виду #include "stdafx.h"

Page 24: Параллельные алгоритмы обработки данных

#include "windows.h" #include "process.h" #include "stdio.h" #include "stdlib.h" #define N 4 #define NumberOfProcessors 2 float Matrix[N][N + 1] = { { 1.0, 1.0, 1.0, 1.0, 1.0 }, { 1.0, 2.0, 1.0, 1.0, 1.0 }, { 2.0, 1.0, 1.0, 1.0, 1.0 }, { 1.0, 2.0, 2.0, 1.0, 1.0 } }; void InitializeMatrix() { printf("Source Matrix\n"); for (unsigned i = 0; i < N; i++) { for (unsigned j = 0; j <= N; j++) printf("%7.4f ", Matrix[i][j]); printf("\n"); } } struct GaussState { HANDLE Starters[NumberOfProcessors]; HANDLE evIterationFinished[NumberOfProcessors]; unsigned ResolveRow; }; GaussState State; unsigned __stdcall ThreadFunction(void *pData) { unsigned Branch = (unsigned)pData;

Page 25: Параллельные алгоритмы обработки данных

unsigned Iteration = 0; while (Iteration + 1 < N) { WaitForSingleObject(State.Starters[Branch], INFINITE); for (unsigned i = Branch + Iteration + 1; i < N; i += NumberOfProcessors) { float Resolver = -Matrix[State.ResolveRow][Iteration] / Matrix[i][Iteration]; for (unsigned j = Iteration; j <= N; j++) Matrix[i][j] = Matrix[State.ResolveRow][j] + Resolver * Matrix[i][j]; } Iteration++; // synchronization SetEvent(State.evIterationFinished[Branch]); if (Branch == 0) { WaitForMultipleObjects(NumberOfProcessors, State.evIterationFinished, TRUE, INFINITE); State.ResolveRow++; for (unsigned i = 0; i < NumberOfProcessors; i++) SetEvent(State.Starters[i]); } } return 0; } int main(int argc, char* argv[]) { UNREFERENCED_PARAMETER(argc);

Page 26: Параллельные алгоритмы обработки данных

UNREFERENCED_PARAMETER(argv); InitializeMatrix(); State.ResolveRow = 0; HANDLE hThreads[NumberOfProcessors]; unsigned i, j, tid; for (i = 0; i < NumberOfProcessors; i++) { State.Starters[i] = CreateEvent(NULL, FALSE, TRUE, NULL); State.evIterationFinished[i] = CreateEvent(NULL, FALSE, FALSE, NULL); } for (i = 0; i < NumberOfProcessors; i++) hThreads[i] = (HANDLE)_beginthreadex( NULL, 0, ThreadFunction, (void *)i, 0, &tid); WaitForMultipleObjects( NumberOfProcessors, hThreads, TRUE, INFINITE); for (i = 0; i < NumberOfProcessors; i++) { CloseHandle(State.Starters[i]); CloseHandle(State.evIterationFinished[i]); CloseHandle(hThreads[i]); } printf("\nTriangle Matrix\n"); for (i = 0; i < N; i++) { for (j = 0; j <= N; j++) printf("%7.4f ", Matrix[i][j]); printf("\n"); } return 0; }

Page 27: Параллельные алгоритмы обработки данных

5. Вычисление скалярного произведения Рассмотрим алгоритм вычисления скалярного произведения двух n-мерных векторов. Пусть даны два

вектора и . Их скалярное произведение вычисляется по формуле:

Оценим время, затрачиваемое на умножение векторов. Пусть — время, затрачиваемое на умножение двух

чисел, — время сложения двух чисел. Тогда время умножения двух n-мерных векторов равно:

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

скалярного произведения векторов. Обозначим количество процессоров в системе, причем ,

где .

Тогда каждый процессор будет вычислять координат результирующего вектора.

Page 28: Параллельные алгоритмы обработки данных

Рисунок 3. Распределение для скалярного произведения.

Оценим время работы параллельного алгоритма следующим образом:

Рассмотрим отношение

Ввиду того, что мы видим, что использование параллельного алгоритма позволяет решить задачу

быстрее. Более того, при , параллельный алгоритм вычисляет скалярное произведение векторов

быстрее последовательного почти в раз.

//////////////////////////////////////// // Параллельное вычисление скалярного произведения #include "stdafx.h" #include <process.h>

Page 29: Параллельные алгоритмы обработки данных

struct Interval { HANDLE hThread; HANDLE evStart; unsigned First; unsigned To; float Result; float *Vector1; float *Vector2; Interval() : First(0), To(0), Vector1(NULL), Vector2(NULL), Result(0.0f), hThread(NULL), evStart(NULL) {} }; unsigned __stdcall ThreadFunction(void *pData); float DotProduct(float *Vector1, float *Vector2, unsigned VectorSize); unsigned GetNumberOfProcessors(); int main(int argc, char *argv[]) { UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); const unsigned VectorSize = 13; float *Vector1 = new float[VectorSize]; float *Vector2 = new float[VectorSize]; printf("Dot product %f\n", DotProduct(Vector1, Vector2, VectorSize)); delete[] Vector2; delete[] Vector1; return 0; } float DotProduct(float *Vector1, float *Vector2,

Page 30: Параллельные алгоритмы обработки данных

unsigned VectorSize) { if (!Vector1 || !Vector2) return 0.0f; unsigned ProcessorCount = GetNumberOfProcessors(); unsigned IntervalLength = VectorSize / ProcessorCount; unsigned Remainder = VectorSize % ProcessorCount; unsigned ProcessorsUsed = (IntervalLength ? ProcessorCount : Remainder); HANDLE *Handles = new HANDLE[ProcessorsUsed]; if (!Handles) return 0.0f; Interval *Intervals = new Interval[ProcessorsUsed]; if (!Intervals) { delete Handles; return 0.0f; } // Nonsignaled manual-reset event HANDLE evStart = CreateEvent(NULL, TRUE, FALSE, NULL); unsigned From = 0; unsigned i; for (i = 0; i < ProcessorCount; i++) { unsigned To = From + IntervalLength; if (Remainder) To++, Remainder--; if (To == From) continue; unsigned tid; Intervals[i].First = From; Intervals[i].To = To;

Page 31: Параллельные алгоритмы обработки данных

Intervals[i].evStart = evStart; Intervals[i].hThread = (HANDLE) _beginthreadex(NULL, 0, ThreadFunction, &Intervals[i], 0, &tid); From = To; } for (i = 0; i < ProcessorsUsed; i++) { Intervals[i].Vector1 = Vector1; Intervals[i].Vector2 = Vector2; Handles[i] = Intervals[i].hThread; } SetEvent(evStart); WaitForMultipleObjects(ProcessorsUsed, Handles, TRUE, INFINITE); float Result = 0.0f; for (i = 0; i < ProcessorsUsed; i++) { Result += Intervals[i].Result; CloseHandle(Intervals[i].hThread); } CloseHandle(evStart); delete[] Intervals; delete[] Handles; return Result; } unsigned __stdcall ThreadFunction(void *pData) { if (pData) { Interval &iv = *(Interval *)pData; WaitForSingleObject(iv.evStart, INFINITE); for (unsigned i = iv.First; i < iv.To; i++)

Page 32: Параллельные алгоритмы обработки данных

iv.Result += iv.Vector1[i] * iv.Vector2[i]; } return 0; } unsigned GetNumberOfProcessors() { DWORD dwProcessAffinityMask, dwSystemAffinityMask; unsigned n = 0; if (GetProcessAffinityMask(GetCurrentProcess(), &dwProcessAffinityMask, &dwSystemAffinityMask)) { for (DWORD dwMask = 0x00000001; dwMask; dwMask <<= 1) if (dwMask & dwProcessAffinityMask) n++; } else n = 1; return n; }

Page 33: Параллельные алгоритмы обработки данных

6. Умножение матриц Задача умножения матриц является базовой макрооперацией для многих задач линейной алгебры. Поэтому

эффективный параллельный алгоритм умножения матриц позволяет значительно увеличить размерность

подобных задач без увеличения времени их решения. Пусть даны две матрицы, и ., произведение

которых необходимо вычислить. Элементы результирующей матрицы определяются по формуле

Оценим время выполнения последовательного алгоритма. Пусть — время, затрачиваемое на умножение

двух чисел, — время сложения двух чисел. Время, затрачиваемое на вычисление одного элемента

результирующей матрицы . В результирующей матрице содержится элементов,

поэтому общее время выполнения умножения матриц равно

Обозначим Отсюда следует, что .

Рассмотрим параллельный алгоритм умножения матриц и оценим время его работы.

Page 34: Параллельные алгоритмы обработки данных

Пусть имеется процессоров, причём, , где — целое. Распараллелим вычисления так, чтобы

каждый процессор вычислял элементов результирующей матрицы. Тогда суммарное время выполнения

можно оценить следующим образом:

Обозначим Сравнивая и , получим: Имеем, что время, затрачиваемое на

умножение матриц с помощью параллельного алгоритма, в раз меньше, чем при использовании

последовательного.

Рассмотрим один из способов распараллеливания алгоритма. Пронумеруем элементы результирующей

матрицы по следующему правилу (нумерация элементов, а также строк и столбцов в данном случае ведётся с

нуля):

Вычисления распределим так, чтобы первый процессор вычислял первые элементов результирующей

матрицы, второй — следующие и т.д. Таким образом, конечная и левая исходная матрица получаются

распределёнными горизонтальными полосами, а правая исходная матрица — вертикальными полосами.

Page 35: Параллельные алгоритмы обработки данных

Рисунок 4. Схема распределения работ при параллельном умножении матриц.

Рассмотрим случай, когда (количество процессоров больше, чем элементов в результирующей

матрице). Незадействованные процессоров можно использовать для ускорения вычисления

отдельных элементов результирующей матрицы. При этом следует использовать способ распараллеливания,

подобный использованному при вычислении скалярного произведения векторов.

//////////////////////////////////////// // Параллельное умножение матриц #include "windows.h" #include "process.h" #include "stdio.h" #define M 3 #define N 4 #define NumberOfProcessors 3 int A[M][N]; int B[N][M]; int C[M][M]; void InititalizeMatices() { unsigned i, j, n;

Page 36: Параллельные алгоритмы обработки данных

printf("\nMatrix A:\n"); for (i = 0, n = 1; i < M; i++) { for (j = 0; j < N; j++, n++) { A[i][j] = n; printf("%4d ", n); } printf("\n"); } printf("\nMatrix B:\n"); for (i = 0, n = 1; i < N; i++) { for (j = 0; j < M; j++, n++) { B[i][j] = n; printf("%4d ", n); } printf("\n"); } } unsigned __stdcall ThreadFunction(void *pData) { unsigned Branch = (unsigned)pData; unsigned Start = M / NumberOfProcessors * Branch; unsigned End = M / NumberOfProcessors * (Branch + 1); for (unsigned i = Start; i < End; i++) for (unsigned j = 0; j < M; j++) { int Value = A[i][0] * B[0][j]; for (unsigned k = 1; k < N; k++) Value += A[i][k] * B[k][j]; C[i][j] = Value;

Page 37: Параллельные алгоритмы обработки данных

} return 0; } int main(int argc, char* argv[]) { UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); assert(M % NumberOfProcessors == 0); InititalizeMatices(); HANDLE hThreads[NumberOfProcessors]; unsigned i, j, n, tid; for (i = 0; i < NumberOfProcessors; i++) hThreads[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunction, (void *)i, 0, &tid); WaitForMultipleObjects(NumberOfProcessors, hThreads, TRUE, INFINITE); for (i = 0; i < NumberOfProcessors; i++) CloseHandle(hThreads[i]); printf("\nMatrix C = A * B:\n"); for (i = 0, n = 1; i < M; i++) { for (j = 0; j < M; j++, n++) printf("%4d ", C[i][j]); printf("\n"); } return 0; }

Page 38: Параллельные алгоритмы обработки данных

Заключение Надеемся, статья помогла Вам немного лучше разобраться в вопросах параллельного программирования, а

приведенные примеры наглядно продемонстрировали некоторые параллельные алгоритмы. Желаем Вам

успехов в создании эффективных программных решений.

Литература

Элементы параллельного программирования / В.А. Вальковский, В.Е. Котов, А.Г. Марчук, Н.Н. Миренков; под ред. В.Е. Котова. — М.: Радио и связь, 1983. — 240с.,интеграл.

Алгоритмы, математическое обеспечение и архитектура многопроцессорных вычислительных систем / Под ред. В.Е. Котова и И. Миклошко — М.: Наука, 1982. — 336 с.

Миренков Н.Н. Параллельное программирование для многомодульных вычислительных систем. — М.: Радио и связь, 1989. — 320с.: ил. — ISBN 5-256-00196-5

Шихаев К.Н. Разностные алгоритмы параллельных вычислительных процессов. — М.: Радио и связь, 1982. — 136с., ил.

Вальковский В.А. Распараллеливание алгоритмов и программ. Структурный подход. М.: Радио и связь, 1989. —176с., ил. — ISBN 5-256-00195-7.

Page 39: Параллельные алгоритмы обработки данных

Об Авторах

Миронов Антон Александрович

Занимается вопросами создания и использования высокопроизводительных параллельных алгоритмов. Один

из ведущих разработчиков в компании Developer Express Inc.

Карпов Андрей Николаевич

Занимается вопросами создания программных решений в области повышения качества и быстродействия

ресурсоемких приложений. Является одним из авторов статического анализатора Viva64 иVivaMP,

предназначенных для верификации 64-битного и параллельного Си/Си++ кода.

Page 40: Параллельные алгоритмы обработки данных