Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.

Предоплата всего

Подписываем
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
Предоплата всего
Подписываем
Содержание
Упражнение 3 - Разработка параллельного алгоритма умножения матрицы на вектор 2
Упражнение 4 - Реализация параллельного алгоритма матрично- векторного умножения 6
Упражнение 3 - Разработка параллельного алгоритма умножения
матрицы на вектор
Принципы распараллеливания
Разработка алгоритмов (а в особенности методов параллельных вычислений) для решения сложных научно-технических задач часто представляет собой значительную проблему. Здесь же мы будем полагать, что вычислительная схема решения нашей задачи умножения матрицы на вектор уже известна. Действия для определения эффективных способов организации параллельных вычислений могут состоять в следующем:
• Выполнить анализ имеющейся вычислительной схемы и осуществить ее разделение (декомпозицию) на части (подзадачи), которые могут быть реализованы в значительной степени независимо друг от друга;
• Выделить для сформированного набора подзадач информационные взаимодействия, которые должны осуществляться в ходе решения исходной поставленной задачи;
• Определить необходимую (или доступную) для решения задачи вычислительную систему и выполнить распределение имеющегося набора подзадач между процессорами системы.
Такие этапы разработки параллельных алгоритмов впервые были предложены Фостером (I. Foster).
При самом общем рассмотрении понятно, что объем вычислений для каждого используемого процессора должен быть примерно одинаков - это позволит обеспечить равномерную вычислительную загрузку (балансировку) процессоров. Кроме того, также понятно, что распределение подзадач между процессорами должно быть выполнено таким образом, чтобы наличие информационных связей (коммуникационных взаимодействий) между подзадачами было минимальным.
Определение подзадач
Для многих методов матричных вычислений характерным является повторение одних и тех же вычислительных действий для разных элементов матриц. Данный момент свидетельствует о наличии параллелизма по данным при выполнении матричных расчетов и, как результат, распараллеливание матричных операций сводится в большинстве случаев к разделению обрабатываемых матриц между процессорами используемой вычислительной системы. Выбор способа разделения матриц приводит к определению конкретного метода параллельных вычислений; существование разных схем распределения данных порождает целый ряд параллельных алгоритмов матричных вычислений.
Дадим кратко общую характеристику распределения данных для матричных алгоритмов - более подробно данный материал содержится в разделе 7 учебного курса. Наиболее общие и широко используемые способы разделения матриц состоят в разбиении данных на полосы (по вертикали или горизонтали) или на прямоугольные фрагменты (блоки).
1. Ленточное разбиение матрицы. При ленточном (block-striped) разбиении каждому процессору выделяется то или иное подмножество строк (rowwise или горизонтальное разбиение) или столбцов (columnwise или вертикальное разбиение) матрицы (рис. 1.7а и 1.7б). Разделение строк и столбцов на полосы в большинстве случаев происходит на непрерывной (последовательной) основе. При таком подходе для горизонтального разбиения по строкам, например, матрица A представляется в виде:
где есть i- я строка матрицы A (предполагается, что количество строк m кратно числу процессоров p, т.е. m = k⋅p). Во всех алгоритмах матричного умножения и умножения матрицы на вектор, которые будут рассмотрены нами в этом и следующем разделах, используется разделение данных на непрерывной основе.
Другой возможный подход к формированию полос состоит в применении той или иной схемы чередования (цикличности) строк или столбцов. Как правило, для чередования используется число процессоров p - в этом случае при горизонтальном разбиении матрица A принимает вид
Циклическая схема формирования полос может оказаться полезной для лучшей балансировки вычислительной нагрузки процессоров (например, при решении системы линейных уравнений с использованием метода Гаусса).
2. Блочное разбиение матрицы. При блочном (checkerboard block) разделении матрица делится на прямоугольные наборы элементов - при этом, как правило, используется разделение на непрерывной основе. Пусть количество процессоров составляет p = s ⋅q , количество строк матрицы является кратным s, а количество столбцов - кратным q, то есть m = k ⋅ s и n = l ⋅ q . Представим исходную матрицу A в виде набора прямоугольных блоков следующим образом (рис. 1.7в):
При таком подходе целесообразно, чтобы вычислительная система имела физическую или, по крайней мере, логическую топологию процессорной решетки из s строк и q столбцов. В этом случае при разделении данных на непрерывной основе процессоры, соседние в структуре решетки, обрабатывают смежные блоки исходной матрицы. Следует отметить, однако, что и для блочной схемы может быть применено циклическое чередование строк и столбцов.
Далее в лабораторной работе будет рассматриваться алгоритм умножения матрицы на вектор, основанный на представлении матрицы непрерывными наборами (горизонтальными полосами) строк. При таком способе разделения данных в качестве базовой подзадачи может быть выбрана операция скалярного умножения одной строки матрицы на вектор.
Выделение информационных зависимостей
Для выполнения базовой подзадачи скалярного произведения процессор должен содержать соответствующую строку матрицы pMatrix и копию вектора pVector. После завершения вычислений каждая базовая подзадача определяет один из элементов вектора результата pResult.
В общем виде схема информационного взаимодействия подзадач в ходе выполняемых вычислений показана на рис. 1.8.
Для объединения результатов расчета и получения полного вектора pResult на каждом из процессоров вычислительной системы необходимо выполнить операцию обобщенного сбора данных, в
которой каждый процессор передает свой вычисленный элемент вектора c всем остальным процессорам. Этот шаг можно выполнить, например, с использованием функции MPI_Allgather из библиотеки MPI (рис. 1.9).
Масштабирование и распределение подзадач по процессорам
В процессе умножения плотной матрицы на вектор количество вычислительных операций для получения скалярного произведения одинаково для всех базовых подзадач. Поэтому в случае, когда
число процессоров p меньше числа базовых подзадач m (p<m), мы можем объединить базовые подзадачи таким образом, чтобы каждый процессор выполнял несколько таких задач, соответствующих непрерывной последовательности строк матрицы pMatrix. В этом случае по окончании вычислений каждая базовая подзадача определяет набор элементов результирующего вектора pResult.
Распределение подзадач между процессорами вычислительной системы может быть выполнено произвольным образом.
Упражнение 4 - Реализация параллельного алгоритма матрично-
векторного умножения
При выполнении этого упражнения Вам будет предложено разработать параллельный алгоритм умножения матрицы на вектор. При работе с этим упражнением Вы
• Познакомитесь с основами MPI, структурой MPI программ и несколькими основными функциями MPI;
• Получите первый опыт разработки параллельных программ.
В параллельных программах, использующих интерфейс передачи сообщений MPI, могут быть выделены следующие основные структурные части:
• Инициализация среды выполнения MPI-программ;
• Основная часть программы, в которой реализуется необходимый алгоритм решения поставленной задачи и в которой осуществляется обмен сообщениями между параллельно выполняемыми частями программы;
• Завершение MPI программы.
Ниже кратко дается характеристика основных понятий MPI.
Понятие параллельной программы
Под параллельной программой в рамках MPI понимается множество одновременно выполняемых процессов. Процессы могут выполняться на разных процессорах, но на одном процессоре могут располагаться и несколько процессов (в этом случае их исполнение осуществляется в режиме разделения времени). В предельном случае для выполнения параллельной программы может использоваться один процессор - как правило, такой способ применяется для начальной проверки правильности параллельной
программы.
Количество процессов и число используемых процессоров определяется в момент запуска параллельной программы средствами среды исполнения MPI - программ и в ходе вычислений меняться не может (в стандарте MPI - 2 предусматривается возможность динамического изменения количества процессов). Все процессы программы последовательно перенумерованы от 0 до p - 1 , где p есть общее количество процессов. Номер процесса именуется рангом процесса.
Понятие коммуникатора и группы процессов
Процессы параллельной программы объединяются в группы. Под коммуникатором в MPI понимается специально создаваемый служебный объект, объединяющий в своем составе группу процессов и ряд дополнительных параметров (контекст), используемых при выполнении операций передачи данных.
Как правило, парные операции передачи данных выполняются для процессов, принадлежащих одному и тому же коммуникатору. Коллективные операции применяются одновременно для всех процессов коммуникатора. Как результат, указание используемого коммуникатора является обязательным для операций передачи данных в MPI.
В ходе вычислений могут создаваться новые и удаляться существующие группы процессов и коммуникаторы. Один и тот же процесс может принадлежать разным группам и коммуникаторам. Все имеющиеся в параллельной программе процессы входят в состав создаваемого по умолчанию коммуникатора с идентификатором MPI_COMM_WORLD.
Задание 1 Открытие проекта ParallelMatrixVectorMult
Откройте проект ParallelMatrixVectorMult, последовательно выполняя следующие шаги:
• Запустите приложение Microsoft Visual Studio 2005, если оно еще не запущено,
• В меню File выполните команду Open→Project/Solution,
• В диалоговом окне Open Project выберите папку с:\MsLabs\ParallelMatrixVectorMult,
• Дважды щелкните на файле ParallelMatrixVectorMult.sln или подсветите его выполните команду Open.
После того, как Вы открыли проект, в окне Solution Explorer (Ctrl+Alt+L) дважды щелкните на файле исходного кода ParallelMV.cpp, как это показано на рисунке 10. После этих действий код, который вам предстоит модифицировать, будет открыт в рабочей области Visual Studio.
Рис. 1.10. Открытие файла ParallelMV.cpp с использованием Solution Explorer
В файле ParallelMV.cpp расположена главная функция (main) будущего параллельного приложения, которая содержит объявления необходимых переменных. Также в файле ParallelMV.cpp расположены функции, перенесенные сюда из проекта, содержащего последовательный алгоритм умножения матрицы на вектор: DummyDataInitialization, RandomDataInitialization, ResultCalculation, PrintMatrix и PrintVector (подробно о назначении этих функций рассказывается в упражнении 2 данной лабораторной работы). Эти функции можно будет использовать и в параллельной программе. Кроме того, помещены заготовки для функций инициализации процесса вычислений (ProcessInitialization) и завершения процесса (ProcessTermination).
Скомпилируйте и запустите приложение стандартными средствами Visual Studio. Убедитесь в том, что в командную консоль выводится приветствие: "Parallel matrix-vector multiplication program".
Задание 2 Инициализация и завершение параллельной программы
Перед тем, как использовать функции MPI в своем приложении, необходимо добавить заголовочный файл MPI в текст программы. Для приложений, написанных на языке C/C++, заголовочный файл имеет имя mpi.h. Этот файл содержит все определения и прототипы функций библиотеки MPI. Добавьте выделенную строку в список подключаемых библиотек в файле исходного кода параллельной программы:
#include <stdlib.h>
#include <stdio.h>
#include <time.h> #include <mpi.h>
В главной функции программы необходимо проинициализировать среду выполнения MPI-программы и завершить ее использование при окончании работы программы. Добавьте выделенный код непосредственно за блоком объявления переменных:
void main(int argc, char* argv[]) {
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double Start, Finish, Duration;
MPI_Init(&argc, &argv);
printf ("Parallel matrix-vector multiplication program\n");
MPI_Finalize();
}
Функция MPI_Init инициализирует среду выполнения MPI-программ. В качестве аргументов этой функции передаются аргументы функции main: количество аргументов командной строки argc и массив, содержащий эти аргументы, argv. Функция MPI_Init должна вызываться в каждой MPI-программе до вызова любой из функций MPI, в каждой программе функция MPI_Init может быть вызвана только один раз.
После выполнения всех необходимых действий, перед завершением выполнения программы, необходимо закрыть среду выполнения MPI-программ. Для завершения среды служит функция MPI_Finalize. Добавьте вызов функции MPI_Finalize последней строчкой вашей программы.
Задание 3 Определение количества процессов
Определение количества процессов в выполняемой параллельной программе осуществляется при помощи функции MPI_Comm_size. В параметрах функции указывается коммуникатор, для которого определяется количество процессов (тем самым, для определения общего числа процессов, доступных для MPI-программы, необходимо указать коммуникатор MPI_COMM_WORLD). Для определения ранга процесса в рамках коммуникатора используется функция MPI_Comm_rank. (напомним, каждому процессу в рамках коммуникатора соответствует уникальное целое число ранг). Заведем переменные целого типа для хранения числа доступных процессов ProcNum и ранга текущего процесса ProcRank. Эти значения обычно используются во всех функциях параллельного приложения. Для того, чтобы эти переменные оказались доступными, объявим ProcNum и ProcRank как глобальные переменные.
Добавьте выделенные строки в соответствующее место в программном коде:
int ProcNum; // Number of available processes
int ProcRank; // Rank of current process
void main(int argc, char* argv[]) {
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double Start, Finish, Duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
printf (“Parallel matrix-vector multiplication program\n”)
MPI_Finalize();
}
Разумно внести такие изменения в код, чтобы печать приветствия и числа доступных процессов выполнял только один процесс, например, процесс с рангом 0. Добавьте выделенный код в приложение:
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf (“Parallel matrix-vector multiplication program\n”)
printf ("Number of abailable processes = %d \n", ProcNum);
}
printf ("Rank of current process = %d \n", ProcRank);
MPI_Finalize();
Задание 4 Ввод размера матрицы и вектора
Теперь перейдем к организации ввода и вывода данных. Как уже известно из материалов упражнения 2, разработка приложения, выполняющего умножение матрицы на вектор, начинается с задания исходных объектов. На самом первом этапе нужно определить размер этих объектов.
Для инициализации вычислительных процессов, как и ранее, служит функция ProcessInitialization:
// Function for memory allocation and initialization of objects elements
void ProcessInitialization(double* &pMatrix, double* &pVector,
double* &pResult, int &Size);
Для определения размеров объектов необходимо реализовать диалог с пользователем. Такой диалог должен проводить только один процесс. Этот процесс назовем ведущим процессом. Обычно в качестве ведущего процесса используется процесс с нулевым рангом. Добавьте выделенный фрагмент кода в тело функции ProcessInitialization:
// Function for memory allocation and initialization of objects elements
void ProcessInitialization(double* &pMatrix, double* &pVector,
double* &pResult, int &Size) {
if (ProcRank == 0) {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
}
В ответ на вопрос, пользователь вводит размер объектов, который затем считывается нулевым процессом параллельной программы из стандартного потока ввода stdin и сохраняется в переменной Size. Итак, после выполнения выделенного фрагмента кода, ведущий процесс параллельной программы хранит в переменной Size введенный размер объектов.
При вводе размера объектов возможно возникновение ошибочных ситуаций. Так, например, в качестве размера объектов пользователь может указать число, меньшее, чем число доступных процессов. Кроме того, для более быстрой и простой подготовки первого варианта параллельной программы будем вначале предполагать, что размер объектов нацело делится на число процессов. В этом случае все процессы обрабатывают одно и то же количество строк исходной матрицы, и получают одно и то же число элементов результирующего вектора (вариант программы для общего случая, когда размер объектов не кратен числу процессов, будет рассмотрен в задании 11). В случае ввода пользователем некорректного размера матрицы и вектора, приложение должно либо завершить свое выполнение, либо продолжать запрашивать размер до тех пор, пока пользователь не введет "правильное" число. Как и ранее, реализуем второй вариант поведения - для этого тот фрагмент кода, который производит ввод размера объектов, поместим в цикл с постусловием:
// Function for memory allocation and initialization of objects elements
void ProcessInitialization(double* &pMatrix, double* &pVector,
double* &pResult, int &Size) {
if (ProcRank == 0) {
do {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
if (Size < ProcNum) {
printf("Size of the objects must be greater than "
"number of processes! \n ");
}
}
while ((Size < ProcNum) || (Size%ProcNum != 0));
}
После того, как значение переменной Size определено корректно, необходимо передать это значение остальным процессам. Для этого используем функцию широковещательной рассылки от одного процесса остальным. Функция имеет следующий интерфейс:
int MPI_Bcast(void *buf, int count, MPI_Datatype type, int root,
MPI_Comm comm),
где
- buf, count, type буфер памяти с отправляемым сообщением (для процесса с рангом root), и для приема сообщений для всех остальных процессов,
- root - ранг процесса, выполняющего рассылку данных,
- comm - коммуникатор, в рамках которого выполняется передача данных.
В нашем случае необходимо передать значение переменной Size с нулевого процесса остальным процессам:
MPI_Bcast(&Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
Т.е
// Function for memory allocation and data initialization
void ProcessInitialization (double* &pMatrix, double* &pVector,
double* &pResult, double* &pProcRows, double* &pProcResult,
int &Size, int &RowNum) {
int RestRows; // Number of rows, that havent been distributed yet
int i; // Loop variable
setvbuf(stdout, 0, _IONBF, 0);
if (ProcRank == 0) {
do {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
if (Size < ProcNum) {
printf("Size of the objects must be greater than number of processes! \n ");
}
}
while (Size < ProcNum);
}
MPI_Bcast(&Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
Добавьте вызов функции инициализации вычислительных процессов:
void main(int argc, char* argv[]) {
int Size; // Sizes of initial matrix and vector
time_t start, finish;
double duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf ("Parallel matrix-vector multiplication program\n");
printf ("Number of available processes = %d \n", ProcNum);
printf ("Rank of current process = %d \n", ProcRank);
}
// Memory allocation and data initialization
ProcessInitialization(pMatrix, pVector, pResult, pProcRows, pProcResult, Size, RowNum);
MPI_Finalize();
}
Задание 5 Ввод исходных данных
После того, как размер объектов определен, можно перейти к выделению памяти и заданию значений элементов матрицы и вектора. Обычно определение начальных данных осуществляется одним из процессов (пусть, как и ранее, этим процессом будет процесс с рангом 0). Далее, согласно схеме параллельных вычислений, изложенной в упражнении 3, исходная матрица распределяется между всеми процессами таким образом, что каждый процесс обрабатывает непрерывную последовательность строк (горизонтальную полосу). Отметим, что первая версия разрабатываемой программы ориентирована на случай, когда размер объектов делится нацело на число процессов, то есть полосы матрицы на всех процессах содержат одно и то же количество строк. Это количество строк будем хранить в переменной RowNum. Адреса буферов памяти, где содержатся горизонтальные полосы строк на каждом из процессов, будем хранить в переменной pProcRows (pProcRows матрица, которая содержит RowNum строк и Size столбцов и хранится построчно). Исходный вектор pVector копируется с процесса с рангом 0 на все процессы. В результате умножения полосы матрицы на вектор, каждый процесс получает RowNum элементов результирующего вектора. Будем хранить эти элементы в массиве pProcResult.
В основной функции программы объявим переменные:
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double* pProcRows; // Stripe of the matrix on the current process
double* pProcResult; // Block of the result vector on the current process
int RowNum; // Number of rows in the matrix stripe
//time_t start, finish;
double Start, Finish, Duration;
Определим значение переменной RowNum, выделим память для хранения объектов и проинициализируем исходные матрицу и вектор на ведущем процессе
void ProcessInitialization (double* &pMatrix, double* &pVector,
double* &pResult, double* &pProcRows, double* &pProcResult,
int &Size, int &RowNum) {
int RestRows; // Number of rows, that havent been distributed yet
int i; // Loop variable
setvbuf(stdout, 0, _IONBF, 0);
if (ProcRank == 0) {
do {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
if (Size < ProcNum) {
printf("Size of the objects must be greater than number of processes! \n ");
}
}
while (Size < ProcNum);
}
MPI_Bcast(&Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
// Determine the number of matrix rows stored on each process
RestRows = Size;
for (i=0; i<ProcRank; i++)
RestRows = RestRows-RestRows/(ProcNum-i);
RowNum = RestRows/(ProcNum-ProcRank);
// Memory allocation
pVector = new double [Size];
pResult = new double [Size];
pProcRows = new double [RowNum*Size];
pProcResult = new double [RowNum];
// Obtain the values of initial objects elements
if (ProcRank == 0) {
}
}
Для задания элементов матрицы и вектора на ведущем процессе мы воспользовались функцией генерации данных RandomDataInitialization, которая была нами разработана при реализации последовательного приложения для умножения матрицы на вектор
// Function for memory allocation and data initialization
void ProcessInitialization (double* &pMatrix, double* &pVector,
double* &pResult, double* &pProcRows, double* &pProcResult,
int &Size, int &RowNum) {
int RestRows; // Number of rows, that havent been distributed yet
int i; // Loop variable
setvbuf(stdout, 0, _IONBF, 0);
if (ProcRank == 0) {
do {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
if (Size < ProcNum) {
printf("Size of the objects must be greater than number of processes! \n ");
}
/*if (Size%ProcNum != 0) {
printf("Size of objects must be divisible by " "number of processes! \n");
}*/
}
while (Size < ProcNum);
}
MPI_Bcast(&Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
// Determine the number of matrix rows stored on each process
RestRows = Size;
for (i=0; i<ProcRank; i++)
RestRows = RestRows-RestRows/(ProcNum-i);
RowNum = RestRows/(ProcNum-ProcRank);
// Memory allocation
pVector = new double [Size];
pResult = new double [Size];
pProcRows = new double [RowNum*Size];
pProcResult = new double [RowNum];
// Obtain the values of initial objects elements
if (ProcRank == 0) {
// Initial matrix exists only on the pivot process
pMatrix = new double [Size*Size];
// Values of elements are defined only on the pivot process
RandomDataInitialization(pMatrix, pVector, Size);
}
}
Задание 6 Завершение процесса вычислений
Для того, чтобы на каждом этапе разработки приложение было завершенным, разработаем функцию для корректной остановки процесса вычислений. Для этого необходимо освободить память, выделенную динамически в процессе выполнения программы. Реализуем соответствующую функцию ProcessTermination. На ведущем процессе выделялась память для хранения исходной матрицы pMatrix, на всех процессах выделялась память для хранения исходного вектора pVector и вектора-результата pResult, а также память для хранения полосы матрицы pProcRows и блока вектора результата pProcResult. Все эти объекты необходимо передать в функцию ProcessTermination в качестве аргументов:
// Function for computational process termination
void ProcessTermination (double* pMatrix, double* pVector, double* pResult,
double* pProcRows, double* pProcResult) {
if (ProcRank == 0)
delete [] pMatrix;
delete [] pVector;
delete [] pResult;
delete [] pProcRows;
delete [] pProcResult;
}
Вызов функции остановки процесса вычислений необходимо выполнить непосредственно перед завершением параллельной программы:
void main(int argc, char* argv[])
{
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double* pProcRows; // Stripe of the matrix on the current process
double* pProcResult; // Block of the result vector on the current process
int RowNum; // Number of rows in the matrix stripe
//time_t start, finish;
double Start, Finish, Duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf ("Parallel matrix-vector multiplication program\n");
printf ("Number of available processes = %d \n", ProcNum);
printf ("Rank of current process = %d \n", ProcRank);
}
// Memory allocation and data initialization
ProcessInitialization(pMatrix, pVector, pResult, pProcRows, pProcResult,
Size, RowNum);
// Process termination
ProcessTermination(pMatrix, pVector, pResult, pProcRows, pProcResult);
MPI_Finalize();
}
Задание 7 Распределение данных между процессами
В соответствии со схемой параллельных вычислений, изложенной в предыдущем упражнении, матрица должна быть разделена между процессами равными горизонтальными полосами, а исходный вектор должен быть скопирован на все процессы. За разделение данных отвечает функция DataDistribution. Ей на вход в качестве аргументов необходимо передать исходные матрицу pMatrix и вектор pVector, адреса буферов для хранения горизонтальных полос матрицы pProcRows, а также размеры объектов (размер матрицы и вектора Size и число полос в горизонтальной полосе RowNum):
// Function for distribution of the initial objects between the processes void DataDistribution(double* pMatrix, double* pProcRows, double* pVector, int Size, int RowNum);
Для копирования вектора на все процессы параллельной программы используем, как и ранее, функцию широковещательной рассылки:
// Function for distribution of the initial objects between the processes
void DataDistribution(double* pMatrix, double* pProcRows, double* pVector,
int Size, int RowNum) {
21
MPI_Bcast(pVector, Size, MPI_DOUBLE, 0, MPI_COMM_WORLD);
}
При нашем подходе матрица хранится в одномерном массиве pMatrix построчно. Следовательно, для того, чтобы разделить матрицу на горизонтальные полосы, необходимо разделить этот массив на блоки одинакового размера и разослать эти блоки процессам. Такая операция носит название обобщенной передачи данных от одного процесса всем процессам MPI программы (распределение данных). Данная операция отличается от широковещательной рассылки тем, что процесс передает всем процессам программы различающиеся данные. Выполнение данной операции может быть обеспечено при помощи функции:
int MPI_Scatter(void *sbuf,int scount,MPI_Datatype stype, void *rbuf,int rcount,MPI_Datatype rtype, int root, MPI_Comm comm), где - sbuf, scount, stype - параметры передаваемого сообщения (scount олво элементов, передаваемых на каждый процесс), определяет кичест - rbuf, rcount, rtype - параметры сообщения, принимаемого в процессах, - root ранг процесса, выполняющего рассылку данных, - comm - коммуникатор, в рамках которого выполняется передача данных.
Добавьте в тело функции DataDistribution вызов функции MPI_Scatter:
// Function for distribution of the initial objects between the processes
void DataDistribution(double* pMatrix, double* pProcRows, double* pVector, int Size, int RowNum) {
MPI_Bcast(pVector, Size, MPI_DOUBLE, 0, MPI_COMM_WORLD); MPI_Scatter(pMatrix, RowNum*Size, MPI_DOUBLE, pProcRows, RowNum*Size, MPI_DOUBLE, 0, MPI_COMM_WORLD);
}
Соответственно, вызывать эту функцию из основной программы нужно непосредственно после вызова функции инициализации вычислительного процесса ProcessInitialization, перед тем, как приступить непосредственно к выполнению матрично-векторного умножения:
void main(int argc, char* argv[]) {
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
time_t start, finish;
double duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf ("Parallel matrix-vector multiplication program\n");
printf ("Number of available processes = %d \n", ProcNum);
printf ("Rank of current process = %d \n", ProcRank);
}
// Memory allocation and data initialization
ProcessInitialization(pMatrix, pVector, pResult, pProcRows, pProcResult, Size, RowNum);
DataDistribution(pMatrix, pProcRows, pVector, Size, RowNum);
MPI_Finalize();
}
Теперь выполним проверку правильности разделения данных между процессами. Для этого после выполнения функции DataDistribution распечатаем исходные матрицу и вектор, а затем полосы матрицы, содержащиеся на каждом из процессов. Добавим в код приложения еще одну функцию, которая служит для проверки правильности выполнения этапа распределения данных, и назовем ее TestDistribution. Для того, чтобы организовать форматированный вывод матрицы и вектора, воспользуемся методами PrintMatrix и PrintVector:
void TestDistribution(double* pMatrix, double* pVector, double* pProcRows,
int Size, int RowNum) {
if (ProcRank == 0) {
printf("Initial Matrix: \n");
PrintMatrix(pMatrix, Size, Size);
printf("Initial Vector: \n");
PrintVector(pVector, Size);
}
MPI_Barrier(MPI_COMM_WORLD);
for (int i=0; i<ProcNum; i++) {
if (ProcRank == i) {
printf("\nProcRank = %d \n", ProcRank);
printf(" Matrix Stripe:\n");
PrintMatrix(pProcRows, RowNum, Size);
printf(" Vector: \n");
PrintVector(pVector, Size);
}
MPI_Barrier(MPI_COMM_WORLD);
}
}
Такой способ проверки правильности выполнения этапов параллельной программы называется отладочной печатью и часто используется в процессе разработки параллельных приложений в том случае, если объем данных, которые необходимо проверить, невелик.
Поясним реализацию функции TestDistribution. В ряде ситуаций независимо выполняемые в процессах вычисления необходимо синхронизировать. Синхронизация процессов, т.е. одновременное достижение процессами тех или иных точек процесса вычислений, обеспечивается при помощи функции MPI:
int MPI_Barrier(MPI_Comm comm);
Функция MPI_Barrier определяет коллективную операции и, тем самым, при использовании должна вызываться всеми процессами используемого коммуникатора. При вызове функции MPI_Barrier выполнение процесса блокируется, продолжение вычислений процесса произойдет только после вызова функции MPI_Barrier всеми процессами коммуникатора. В функции TestDistribution функция MPI_Barrier используется для того, чтобы обеспечить порядок печати. Так, сначала необходимо напечатать исходные объекты на ведущем процессе. Для того, чтобы в то же самое время свою печать не вели другие процессы параллельной программы, вызвана функция MPI_Barrier. Выполнение действий на других процессах начнется только после того, как ведущий процесс вызовет MPI_Barrier по окончании печати исходных объектов. Далее, та же схема используется для того, чтобы процессы печатали свои полосы матриц по порядку (сначала свою полосу печатает процесс с рангом 0, далее процесс с рангом 1 и т.д.).
Задание 8 Реализация умножения матрицы на вектор
Выполнение умножения происходит в функции ParallelResultCalculation. Для вычисления блока результирующего вектора необходимо иметь доступ к полосе матрицы pProcRows, вектору pVector и блоку результирующего вектора pProcResult. Кроме того, необходимо знать размеры этих объектов. Таким образом, в функцию ParallelResultCalculation необходимо передать следующие аргументы:
void ParallelResultCalculation(double* pProcRows, double* pVector, double* pProcResult, int Size, int RowNum);
Для получения значения каждого конкретного элемента результирующего вектора необходимо, как и в последовательном алгоритме, выполнить скалярное умножение строки матрицы на вектор-аргумент. Отличие от последовательного кода состоит только в том, что процесс работает не с самой матрицей, а ее частью pProcRows и обрабатывает не Size, а только RowNum строк.
// Process rows and vector multiplication
void ParallelResultCalculation(double* pProcRows, double* pVector, double* pProcResult, int Size, int RowNum) {
int i, j; // Loop variables
for (i=0; i<RowNum; i++) {
pProcResult[i] = 0;
for (j=0; j<Size; j++)
pProcResult[i] += pProcRows[i*Size+j]*pVector[j];
}
}
Вызывать функцию ParallelResultCalculation в функции main после функции DataDistribution
// Distributing the initial objects between the processes DataDistribution(pMatrix, pProcRows, pVector, Size, RowNum);
// Process rows and vector multiplication ParallelResultCalculation(pProcRows, pVector, pProcResult, Size, RowNum);
Задание 9 Сбор результатов
На следующем этапе необходимо собрать результирующий вектор из частей, расположенных на разных процессах. Как уже говорилось в упражнении 3, в библиотеке MPI предусмотрена соответствующая функция MPI_Allgather, которая собирает из блоков, расположенных на разных процессах коммуникатора, единый массив, и копирует его на все процессы. Функция имеет следующий интерфейс:
int MPI_Allgather(void *sbuf,int scount,MPI_Datatype stype,
void *rbuf,int rcount,MPI_Datatype rtype, MPI_Comm comm),
где
- sbuf, scount, stype - параметры передаваемого сообщения,
- rbuf, rcount, rtype - параметры принимаемого сообщения,
- comm - коммуникатор, в рамках которого выполняется передача данных.
За сбор результатов отвечает функция ResultReplication, которая будет состоять только из вызова функции MPI_Allgather:
// Result vector replication
void ResultReplication(double* pProcResult, double* pResult, int Size,
int RowNum) {
int i; // Loop variable
int *pReceiveNum; // Number of elements, that current process sends
int *pReceiveInd; /* Index of the first element from current process
in result vector */
int RestRows=Size; // Number of rows, that havent been distributed yet
//Alloc memory for temporary objects
pReceiveNum = new int [ProcNum];
pReceiveInd = new int [ProcNum];
//Define the disposition of the result vector block of current processor
pReceiveInd[0] = 0;
pReceiveNum[0] = Size/ProcNum;
for (i=1; i<ProcNum; i++) {
RestRows -= pReceiveNum[i-1];
pReceiveNum[i] = RestRows/(ProcNum-i);
pReceiveInd[i] = pReceiveInd[i-1]+pReceiveNum[i-1];
}
//Gather the whole result vector on every processor
MPI_Allgatherv(pProcResult, pReceiveNum[ProcRank], MPI_DOUBLE, pResult,
pReceiveNum, pReceiveInd, MPI_DOUBLE, MPI_COMM_WORLD);
//Free the memory
delete [] pReceiveNum;
delete [] pReceiveInd;
}
Вызов функции из функции main:
ParallelResultCalculation(pProcRows, pVector, pProcResult, Size, RowNum);
// Result replication
ResultReplication(pProcResult, pResult, Size, RowNum);
Задание 10 Проверка правильности работы программы
Теперь, после выполнения функции сбора, необходимо проверить правильность выполнения алгоритма. Для этого разработаем функцию TestResult, которая сравнит результаты последовательного и параллельного алгоритмов. Для выполнения последовательного алгоритма можно использовать функцию SerialResultCalculation, разработанную в упражнении 2. Результат работы этой функции сохраним в векторе pSerialResult, а затем поэлементно сравним этот вектор с вектором pResult, полученным при помощи параллельного алгоритма. Функция TestResult должна иметь доступ к исходным матрице pMatrix и вектору pVector, а значит может быть выполнена только на ведущем процессе:
// Testing the result of parallel matrix-vector multiplication
void TestResult(double* pMatrix, double* pVector, double* pResult,
int Size) {
// Buffer for storing the result of serial matrix-vector multiplication
double* pSerialResult;
// Flag, that shows wheather the vectors are identical or not
int equal = 0;
int i; // Loop variable
if (ProcRank == 0) {
pSerialResult = new double [Size];
SerialResultCalculation(pMatrix, pVector, pSerialResult, Size);
for (i=0; i<Size; i++) {
if (pResult[i] != pSerialResult[i])
equal = 1;
}
if (equal == 1)
printf("The results of serial and parallel algorithms are NOT identical. Check your code.");
else
printf("The results of serial and parallel algorithms are identical.");
}
}
Результатом работы этой функции является печать диагностического сообщения. Используя эту функцию, можно проверять результат работы параллельного алгоритма независимо от того, насколько велики исходные объекты при любых значениях исходных данных. Вызов функции TestResult идет после функции ResultReplication в функции main
ResultReplication(pProcResult, pResult, Size, RowNum);
TestResult(pMatrix, pVector, pResult, Size);
Задание 12 Проведение вычислительных экспериментов
Основная задача при реализации параллельных алгоритмов решения сложных вычислительных задач обеспечить ускорение вычислений (по сравнению с последовательным алгоритмом) за счет использования нескольких процессоров. Время выполнения параллельного алгоритма должно быть меньше, чем при выполнении последовательного алгоритма. Определим время выполнения параллельного алгоритма. Для этого добавим в программный код замеры времени. Следует отметить, что в MPI для замеров времени имеется специальная функция:
MPI_Wtime();
Поскольку параллельный алгоритм включает этап распределения данных, вычисления блока частичных результатов на каждом процессе и сбора результата, то отсчет времени должен начинаться непосредственно перед вызовом функции DataDistribution, и останавливаться сразу после выполнения функции ResultReplication: (окончательный вид функции main)
void main(int argc, char* argv[])
{
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double* pProcRows; // Stripe of the matrix on the current process
double* pProcResult; // Block of the result vector on the current process
int RowNum; // Number of rows in the matrix stripe
double Start, Finish, Duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf ("Parallel matrix-vector multiplication program\n");
printf ("Number of available processes = %d \n", ProcNum);
printf ("Rank of current process = %d \n", ProcRank);
}
// Memory allocation and data initialization
ProcessInitialization(pMatrix, pVector, pResult, pProcRows, pProcResult,
Size, RowNum);
Start = MPI_Wtime();
// Distributing the initial objects between the processes
DataDistribution(pMatrix, pProcRows, pVector, Size, RowNum);
// Process rows and vector multiplication
ParallelResultCalculation(pProcRows, pVector, pProcResult, Size, RowNum);
// Result replication
ResultReplication(pProcResult, pResult, Size, RowNum);
Finish = MPI_Wtime();
Duration = Finish-Start;
TestResult(pMatrix, pVector, pResult, Size);
if (ProcRank == 0) {
printf("Time of execution = %f\n", Duration);
}
// Process termination
ProcessTermination(pMatrix, pVector, pResult, pProcRows, pProcResult);
MPI_Finalize();
}
Очевидно, что таким образом будет распечатано то время, которое было затрачено на выполнение вычислений нулевым процессом. Возможно, что время выполнения алгоритма другими процессами немного от него отличается. Но на этапе разработки параллельного алгоритма мы особое внимание уделили равномерной загрузке (балансировке) процессов, поэтому теперь у нас есть о снования полагать, что время выполнения алгоритма другими процессами несущественно отличается от приведенного.
Контрольные вопросы
• В качестве времени выполнения параллельного алгоритма было выбрано время, затраченное первым процессом. Как нужно модифицировать код для того, чтобы выбрать максимальное среди времен, полученных на всех процессах?
• Насколько сильно отличаются время, затраченное на выполнение последовательного и параллельного алгоритма? Почему?
• Получилось ли ускорение при матрице размером 10 на 10? Почему?
• Насколько хорошо совпадают время, полученное теоретически, и реальное время выполнения алгоритма? В чем может состоять причина несовпадений?
29 30
Задания для самостоятельной работы
1. Изучите параллельный алгоритм умножения матрицы на вектор, основанный на ленточном вертикальном разделении матрицы. Напишите программу, реализующую этот алгоритм.
2. Изучите параллельный алгоритм умножения матрицы на вектор, основанный на блочном разделении матрицы. Напишите программу, реализующую этот алгоритм.
Приложение 2 Программный код параллельного приложения для умножения матрицы на вектор
#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
#include "windows.h"
#include"iostream"
int ProcNum = 0; // Number of available processes
int ProcRank = 0; // Rank of current process
// Function for simple definition of matrix and vector elements
void DummyDataInitialization (double* pMatrix, double* pVector, int Size) {
int i, j; // Loop variables
for (i=0; i<Size; i++) {
pVector[i] = 1;
for (j=0; j<Size; j++)
pMatrix[i*Size+j] = i;
}
}
// Function for random definition of matrix and vector elements
void RandomDataInitialization(double* pMatrix, double* pVector, int Size) {
int i, j; // Loop variables
srand(unsigned(clock()));
for (i=0; i<Size; i++) {
pVector[i] = rand()/double(1000);
for (j=0; j<Size; j++)
pMatrix[i*Size+j] = rand()/double(1000);
}
}
// Function for memory allocation and data initialization
void ProcessInitialization (double* &pMatrix, double* &pVector,
double* &pResult, double* &pProcRows, double* &pProcResult,
int &Size, int &RowNum) {
int RestRows; // Number of rows, that havent been distributed yet
int i; // Loop variable
setvbuf(stdout, 0, _IONBF, 0);
if (ProcRank == 0) {
do {
printf("\nEnter size of the initial objects: ");
scanf("%d", &Size);
if (Size < ProcNum) {
printf("Size of the objects must be greater than number of processes! \n ");
}
/*if (Size%ProcNum != 0) {
printf("Size of objects must be divisible by " "number of processes! \n");
}*/
}
while (Size < ProcNum);
}
MPI_Bcast(&Size, 1, MPI_INT, 0, MPI_COMM_WORLD);
// Determine the number of matrix rows stored on each process
RestRows = Size;
for (i=0; i<ProcRank; i++)
RestRows = RestRows-RestRows/(ProcNum-i);
RowNum = RestRows/(ProcNum-ProcRank);
// Memory allocation
pVector = new double [Size];
pResult = new double [Size];
pProcRows = new double [RowNum*Size];
pProcResult = new double [RowNum];
// Obtain the values of initial objects elements
if (ProcRank == 0) {
// Initial matrix exists only on the pivot process
pMatrix = new double [Size*Size];
// Values of elements are defined only on the pivot process
RandomDataInitialization(pMatrix, pVector, Size);
}
}
// Function for distribution of the initial objects between the processes
void DataDistribution(double* pMatrix, double* pProcRows, double* pVector,
int Size, int RowNum) {
int *pSendNum; // The number of elements sent to the process
int *pSendInd; // The index of the first data element sent to the process
int RestRows=Size; // Number of rows, that havent been distributed yet
MPI_Bcast(pVector, Size, MPI_DOUBLE, 0, MPI_COMM_WORLD);
// Alloc memory for temporary objects
pSendInd = new int [ProcNum];
pSendNum = new int [ProcNum];
// Define the disposition of the matrix rows for current process
RowNum = (Size/ProcNum);
pSendNum[0] = RowNum*Size;
pSendInd[0] = 0;
for (int i=1; i<ProcNum; i++) {
RestRows -= RowNum;
RowNum = RestRows/(ProcNum-i);
pSendNum[i] = RowNum*Size;
pSendInd[i] = pSendInd[i-1]+pSendNum[i-1];
}
// Scatter the rows
MPI_Scatterv(pMatrix , pSendNum, pSendInd, MPI_DOUBLE, pProcRows,
pSendNum[ProcRank], MPI_DOUBLE, 0, MPI_COMM_WORLD);
// Free the memory
delete [] pSendNum;
delete [] pSendInd;
}
// Result vector replication
void ResultReplication(double* pProcResult, double* pResult, int Size,
int RowNum) {
int i; // Loop variable
int *pReceiveNum; // Number of elements, that current process sends
int *pReceiveInd; /* Index of the first element from current process
in result vector */
int RestRows=Size; // Number of rows, that havent been distributed yet
//Alloc memory for temporary objects
pReceiveNum = new int [ProcNum];
pReceiveInd = new int [ProcNum];
//Define the disposition of the result vector block of current processor
pReceiveInd[0] = 0;
pReceiveNum[0] = Size/ProcNum;
for (i=1; i<ProcNum; i++) {
RestRows -= pReceiveNum[i-1];
pReceiveNum[i] = RestRows/(ProcNum-i);
pReceiveInd[i] = pReceiveInd[i-1]+pReceiveNum[i-1];
}
//Gather the whole result vector on every processor
MPI_Allgatherv(pProcResult, pReceiveNum[ProcRank], MPI_DOUBLE, pResult,
pReceiveNum, pReceiveInd, MPI_DOUBLE, MPI_COMM_WORLD);
//Free the memory
delete [] pReceiveNum;
delete [] pReceiveInd;
}
// Function for sequential matrix-vector multiplication
void SerialResultCalculation(double* pMatrix, double* pVector, double* pResult, int Size) {
int i, j; // Loop variables
for (i=0; i<Size; i++) {
pResult[i] = 0;
for (j=0; j<Size; j++)
pResult[i] += pMatrix[i*Size+j]*pVector[j];
}
}
// Process rows and vector multiplication
void ParallelResultCalculation(double* pProcRows, double* pVector, double* pProcResult, int Size, int RowNum) {
int i, j; // Loop variables
for (i=0; i<RowNum; i++) {
pProcResult[i] = 0;
for (j=0; j<Size; j++)
pProcResult[i] += pProcRows[i*Size+j]*pVector[j];
}
}
// Function for formatted matrix output
void PrintMatrix (double* pMatrix, int RowCount, int ColCount) {
int i, j; // Loop variables
for (i=0; i<RowCount; i++) {
for (j=0; j<ColCount; j++)
printf("%7.4f ", pMatrix[i*ColCount+j]);
printf("\n");
}
}
// Function for formatted vector output
void PrintVector (double* pVector, int Size) {
int i;
for (i=0; i<Size; i++)
printf("%7.4f ", pVector[i]);
}
void TestDistribution(double* pMatrix, double* pVector, double* pProcRows,
int Size, int RowNum) {
if (ProcRank == 0) {
printf("Initial Matrix: \n");
PrintMatrix(pMatrix, Size, Size);
printf("Initial Vector: \n");
PrintVector(pVector, Size);
}
MPI_Barrier(MPI_COMM_WORLD);
for (int i=0; i<ProcNum; i++) {
if (ProcRank == i) {
printf("\nProcRank = %d \n", ProcRank);
printf(" Matrix Stripe:\n");
PrintMatrix(pProcRows, RowNum, Size);
printf(" Vector: \n");
PrintVector(pVector, Size);
}
MPI_Barrier(MPI_COMM_WORLD);
}
}
// Fuction for testing the results of multiplication of the matrix stripe
// by a vector
void TestPartialResults(double* pProcResult, int RowNum) {
int i; // Loop variables
for (i=0; i<ProcNum; i++) {
if (ProcRank == i) {
printf("\nProcRank = %d \n Part of result vector: \n", ProcRank);
PrintVector(pProcResult, RowNum);
}
MPI_Barrier(MPI_COMM_WORLD);
}
}
// Testing the result of parallel matrix-vector multiplication
void TestResult(double* pMatrix, double* pVector, double* pResult,
int Size) {
// Buffer for storing the result of serial matrix-vector multiplication
double* pSerialResult;
// Flag, that shows wheather the vectors are identical or not
int equal = 0;
int i; // Loop variable
if (ProcRank == 0) {
pSerialResult = new double [Size];
SerialResultCalculation(pMatrix, pVector, pSerialResult, Size);
for (i=0; i<Size; i++) {
if (pResult[i] != pSerialResult[i])
equal = 1;
}
if (equal == 1)
printf("The results of serial and parallel algorithms are NOT identical. Check your code.");
else
printf("The results of serial and parallel algorithms are identical.");
}
}
// Function for computational process termination
void ProcessTermination (double* pMatrix, double* pVector, double* pResult,
double* pProcRows, double* pProcResult) {
if (ProcRank == 0)
delete [] pMatrix;
delete [] pVector;
delete [] pResult;
delete [] pProcRows;
delete [] pProcResult;
}
void main(int argc, char* argv[])
{
double* pMatrix; // The first argument - initial matrix
double* pVector; // The second argument - initial vector
double* pResult; // Result vector for matrix-vector multiplication
int Size; // Sizes of initial matrix and vector
double* pProcRows; // Stripe of the matrix on the current process
double* pProcResult; // Block of the result vector on the current process
int RowNum; // Number of rows in the matrix stripe
double Start, Finish, Duration;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &ProcNum);
MPI_Comm_rank(MPI_COMM_WORLD, &ProcRank);
if (ProcRank == 0) {
printf ("Parallel matrix-vector multiplication program\n");
printf ("Number of available processes = %d \n", ProcNum);
printf ("Rank of current process = %d \n", ProcRank);
}
// Memory allocation and data initialization
ProcessInitialization(pMatrix, pVector, pResult, pProcRows, pProcResult,
Size, RowNum);
Start = MPI_Wtime();
// Distributing the initial objects between the processes
DataDistribution(pMatrix, pProcRows, pVector, Size, RowNum);
// Process rows and vector multiplication
ParallelResultCalculation(pProcRows, pVector, pProcResult, Size, RowNum);
// Result replication
ResultReplication(pProcResult, pResult, Size, RowNum);
Finish = MPI_Wtime();
Duration = Finish-Start;
TestResult(pMatrix, pVector, pResult, Size);
if (ProcRank == 0) {
printf("Time of execution = %f\n", Duration);
}
// Process termination
ProcessTermination(pMatrix, pVector, pResult, pProcRows, pProcResult);
MPI_Finalize();
}