Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Практикум по параллельному программированию с использованием электронного задачника Programming Taskbook for MPI

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
41
[Pascal]
if rank > 0 then begin GetR(a); MPI_Send(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, s); PutR(a); end
При запуске программы будет выведено сообщение о верном реше-
нии, а после пяти запусков — о том, что задание выполнено (рис. 11).
Рис. 11. Успешное выполнение задания MPIBegin17
Полученную программу можно упростить, если в разделе else вос­пользоваться вспомогательной вещественной переменной b для получения данных из подчиненных процессов. Это позволит разместить оператор ввода (
pt >> a для C++, GetR(a) для Паскаля) перед последним условным
оператором, а также даст возможность выполнить все действия в разделе else в единственном цикле. Приведем соответствующий вариант решения:
[C++]
void Solve()
{
Task("MPIBegin17");
int flag;
MPI_Initialized(&flag);
if (flag == 0)
return;
int rank, size;
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
42 Часть I. Учебные задания и примеры их выполнения
double a; MPI_Status s; pt >> a; if (rank > 0) { MPI_Send(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(&a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, &s); pt << a; } else for (int i = 1; i < size; ++i) { double b; MPI_Recv(&b, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, &s); pt << b; MPI_Send(&a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD); } }
[Pascal]
program MPIBegin17; uses PT4, MPI; var flag, size, rank: integer; a, b: real; i: integer; s: MPI_Status; begin Task('MPIBegin17'); MPI_Initialized(flag); if flag = 0 then exit; MPI_Comm_size(MPI_COMM_WORLD, size); MPI_Comm_rank(MPI_COMM_WORLD, rank); GetR(a); if rank > 0 then begin MPI_Send(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD); MPI_Recv(@a, 1, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, s); PutR(a); end else for i := 1 to size – 1 do begin MPI_Recv(@b, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD, s);
43
PutR(b);
MPI_Send(@a, 1, MPI_DOUBLE, i, 0, MPI_COMM_WORLD);
end;
end.
Заметим, что более эффективное решение этого задания можно полу­чить, используя коллективные операции пересылки данных (см. п. 7.6).
3.2. Операции редукции и составные типы данных:
MPIBegin52
В следующем задании мы познакомимся с коллективными операция­ми редукции и особенностями использования в MPI-программах состав­ных типов данных.
MPIBegin52. В каждом процессе дан набор из K + 5 чисел, где K
количество процессов. Используя функцию MPI_Allreduce для операции MPI_MINLOC, найти минимальное значение среди элементов данных наборов с одним и тем же порядковым номером и ранг процесса, содер­жащего минимальное значение. Вывести в главном процессе минимумы, а в остальных процессах — ранги процессов, содержащих эти миниму­мы.
Большая группа функций MPI предназначена для организации кол­лективного взаимодействия процессов (см. п. 7.6). «Коллективные» MPI- функции, в отличие от ранее рассмотренных функций MPI_Send и MPI_Recv, позволяют организовать обмен сообщениями не между двумя отдельными процессами (отправителем и получателем), а между всеми процессами, входящими в некоторый коммуникатор. В частности, при ис­пользовании коммуникатора MPI_COMM_WORLD можно организовать коллективный обмен сообщениями между всеми запущенными процесса­ми параллельной программы.
Среди коллективных MPI-функций выделяют группу функций, обес­печивающих выполнение коллективных операций редукции, т. е. операций, связанных с пересылкой не исходных данных, а результатов их обработки некоторой групповой операцией: нахождением суммы MPI_SUM, произве­дения MPI_PROD, максимального MPI_MAX или минимального MPI_MIN значения и т. д. — всего в стандарте MPI предусмотрено 12 операций ре­дукции (см. п. 7.1), кроме того, программист может определять и свои соб­ственные операции. Среди операций редукции особое место занимают опе­рации MPI_MAXLOC и MPI_MINLOC, позволяющие найти не только максимальный или минимальный элемент среди элементов, предоставлен­ных каждым процессом, но и его номер (в качестве номера обычно исполь­зуется ранг процесса, предоставившего этот экстремальный элемент).
При запуске программы-заготовки, созданной для выполнения зада­ния MPIBegin52, мы увидим на экране окно задачника, подобное приве­денному на рисунке 12.
44 Часть I. Учебные задания и примеры их выполнения
Рис. 12. Ознакомительный запуск задания MPIBegin52
Коллективная операция редукции может применяться одновременно к нескольким наборам данных. Если каждый процесс предоставляет массив чисел (одного и того же размера), то операция редукции применяется по отдельности к элементам предоставленных массивов с одним и тем же ин­дексом; в результате будет получен массив того же размера, каждый эле­мент которого будет являться результатом применения операции редукции к элементам исходных массивов с этим же индексом. В зависимости от ис­пользуемой MPI-функции полученный массив может быть переслан како­му-либо конкретному процессу (функция MPI_Reduce) или всем процес­сам данного коммуникатора (функция MPI_Allreduce).
При использовании операций MPI_MAXLOC и MPI_MINLOC исход­ные наборы данных должны содержать пары чисел: собственно число, ко­торое надо обработать, и его номер. Поэтому в программе необходимо оп­ределить вспомогательный тип данных (запись в Паскале, структуру в C++) для хранения таких пар. В нашем случае должны обрабатываться ве­щественные числа, поэтому первый элемент пары будет вещественным, а второй — целым:
[C++]
struct MINLOC_Data
{
double a;
int n;
};
[Pascal]
type
MINLOC_Data = record
a: real;
45
n: integer; end;
Для хранения исходных данных в каждом процессе должен быть вы­делен массив элементов типа MINLOC_Data, и такой же массив должен использоваться для хранения результатов выполнения операции редукции. Размер набора данных, который придется хранить в этих массивах, заранее неизвестен, так как он связан с количеством процессов параллельной про­граммы. Поэтому можно либо выделять память для массивов динамически (после того как программе станет известно число процессов size), либо ис­пользовать статические массивы, размер которых окажется достаточным для любых наборов исходных данных. При выполнении задания MPIBe­gin52 мы будем использовать статические массивы (особенности, связан­ные с использованием динамических массивов, будут рассмотрены в сле­дующем пункте). Запустив созданную программу-заготовку несколько раз, мы можем убедиться в том, что для данного задания количество процессов может меняться в диапазоне от 3 до 5. Таким образом, учитывая, что раз­мер наборов исходных данных равен K + 5, где K — количество процессов, нам достаточно описать массивы размера 10:
[C++]
MINLOC_Data d[10], res[10];
[Pascal]
var
d, res: array[0..9] of MINLOC_Data;
Для большего единообразия программ мы будем использовать в Пас­кале индексацию массивов от 0. Заметим, что именно такая индексация используется в языках Delphi Pascal, Free Pascal и PascalABC.NET для ди- намических массивов, которые в дальнейшем мы также будем применять в наших программах.
Инициализация исходного массива d должна выполняться в каждом процессе параллельной программы (в программе на Паскале необходимо дополнительно описать переменную i целого типа):
[C++]
for (int i = 0; i < size + 5; ++i)
{
pt >> d[i].a;
d[i].n = rank;
}
[Pascal]
for i := 0 to size + 4 do
begin
GetR(d[i].a);
d[i].n := rank;
end;
46 Часть I. Учебные задания и примеры их выполнения
Рис. 13. Окно задачника с информацией об ошибках ввода-вывода
Выполнив запуск этого варианта программы, мы получим сообщение об ошибке «Выведены не все результирующие данные» (рис. 13). Действи­тельно, выполнив ввод всех исходных данных, мы не вывели результаты ни в одном процессе. Напомним, что при обнаружении ошибок в подчи­ненных процессах задачник не анализирует состояние главного процесса, поэтому в списке ошибочных процессов отсутствует процесс 0.
Перед выводом результатов необходимо выполнить соответствующую коллективную операцию редукции. Она должна быть выполнена во всех процессах, после чего в главном процессе (ранга 0) надо вывести поле a каждого элемента результирующего массива res (т. е. минимальное значе­ние, выбранное из всех элементов исходных массивов с данным индексом), а в остальных (подчиненных) процессах — поле n (т. е. ранг процесса с этим минимальным значением):
[C++]
MPI_Allreduce(d, res, size + 5, MPI_DOUBLE_INT, MPI_MINLOC,
MPI_COMM_WORLD);
for (int i = 0; i < size + 5; ++i)
if (rank == 0)
pt << res[i].a;
else
pt << res[i].n;
47
[Pascal]
MPI_Allreduce(@d[0], @res[0], size + 5, MPI_DOUBLE_INT, MPI_MINLOC, MPI_COMM_WORLD); for i := 0 to size + 4 do if rank = 0 then PutR(res[i].a) else PutN(res[i].n);
Обратите внимание на два важных момента. Во-первых, массивы ис­ходных и результирующих данных передаются в MPI-функции как указа­тели на их начальный элемент, поэтому в варианте для Паскаля использу­ется операция @ взятия адреса элемента массива с индексом 0 (заметим, что в средах Delphi и Lazarus можно использовать более простые выраже­ния: @d и @res). Во-вторых, имя типа, указываемое в качестве четвертого параметра, должно соответствовать типу элементов обрабатываемых мас­сивов (в данном случае надо указать имя MPI_DOUBLE_INT, соответст­вующее структуре из двух полей — вещественного и целого).
При запуске полученной программы в среде C++ будет выведено со­общение о верном решении, а после пяти запусков — сообщение о том, что задание выполнено.
Однако при запуске варианта программы для языка Паскаль в среде Lazarus будет выведено сообщение об ошибочном решении (рис. 14).
Рис. 14. Окно задачника с информацией об ошибочном решении
Если проанализировать полученные результирующие данные, то можно заметить, что минимальное значение и его номер были правильно определены для набора из первых элементов исходных массивов, тогда как для других элементов были получены либо неверные результаты, либо ну­левые значения, не имеющие отношения к исходным данным.
48 Часть I. Учебные задания и примеры их выполнения
Причина ошибочного поведения связана с тем, что при стандартных настройках компилятора Free Pascal, используемого в среде Lazarus, не выполняется выравнивание составных данных, в то время как в функциях библиотеки MPI предполагается, что выравнивание производится. Чтобы убедиться в этом, достаточно вызвать функцию MPI_Type_extent, которая позволяет определить размер памяти (в байтах), используемый для любого типа библиотеки MPI. Размер возвращается во втором параметре этой функции, имеющем специальный тип библиотеки MPI — MPI_Aint. Для вывода полученного значения воспользуемся новой возможностью задач­ника Programming Taskbook, появившейся в его версии 4.9, — функцией Show, которая позволяет выводить информацию в раздел отладки окна за­дачника. Опишем в программе переменную k типа MPI_Aint и добавим в конец программы следующий фрагмент:
[Pascal]
MPI_Type_extent(MPI_DOUBLE_INT, k);
Show('k = ', k);
При запуске этого варианта программы в окне задачника появится раздел отладки, и в нем будет выведено значение переменной k с предва­ряющим комментарием (рис. 15). Обратите внимание на то, что значение k выводится несколько раз, так как процедура Show выполняется в каждом процессе параллельного приложения.
Рис. 15. Окно задачника с отладочной информацией
49
Таким образом, в библиотеке MPI предполагается, что для типа
MPI_DOUBLE_INT отводится 16 байт, т. е. для полей этой записи выпол-
няется выравнивание по ширине 8 байт. Если же указанное выравнивание не проводить, то запись, состоящая из одного вещественного числа двой­ной точности (8 байт) и одного целого числа (4 байта), будет занимать только 12 байт, и в результате все элементы массива таких записей, начи­ная со второго элемента, будут располагаться в памяти не в тех позициях, которые предполагает функция MPI_Allreduce. Этим и объясняется тот факт, что первые элементы исходных массивов были обработаны правиль­но, а остальные нет. В случае использования среды Visual Studio для языка C++, а также сред Delphi и PascalABC.NET для языка Паскаль, ошибки не возникает, так как компиляторы этих сред по умолчанию выполняют вы­равнивание данных по ширине 8 байт.
После того как причина ошибки выяснена, исправить ее для среды
Lazarus не представляет труда. Достаточно добавить в запись MINLOC_Data еще одно, «фиктивное» поле, имеющее размер 4 байта и дополняющее, таким образом, размер записи до требуемых 16 байт:
[Pascal]
type MINLOC_Data = record a: real; n, not_used: integer; end;
Заметим, что вместо добавления фиктивного поля можно было бы ус-
тановить для записи MINLOC_Data режим выравнивания по ширине
8 байт, указав перед ее описанием директиву компилятора Free Pascal {$PackRecords 8}.
В заключение приведем полный текст решения задания MPIBegin52
на языках C++ и Паскаль:
[C++]
struct MINLOC_Data { double a; int n; };
void Solve() { Task("MPIBegin52"); int flag; MPI_Initialized(&flag); if (flag == 0) return;
50 Часть I. Учебные задания и примеры их выполнения
int rank, size;
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MINLOC_Data d[10], res[10];
for (int i = 0; i < size + 5; ++i)
{
pt >> d[i].a;
d[i].n = rank;
}
MPI_Allreduce(d, res, size + 5, MPI_DOUBLE_INT, MPI_MINLOC,
MPI_COMM_WORLD);
for (int i = 0; i < size + 5; ++i)
if (rank == 0)
pt << res[i].a;
else
pt << res[i].n;
}
[Pascal]
program MPIBegin52;
uses PT4, MPI;
type
MINLOC_Data = record
a: real;
n, not_used: integer;
end;
var
flag, size, rank: integer;
d, res: array[0..9] of MINLOC_Data;
i: integer;
begin
Task('MPIBegin52');
MPI_Initialized(flag);
if flag = 0 then exit;
MPI_Comm_size(MPI_COMM_WORLD, size);
MPI_Comm_rank(MPI_COMM_WORLD, rank);
for i := 0 to size + 4 do
begin
GetR(d[i].a);
d[i].n := rank;
end;
MPI_Allreduce(@d[0], @res[0], size + 5, MPI_DOUBLE_INT,
MPI_MINLOC, MPI_COMM_WORLD);
for i := 0 to size + 4 do
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]