Скачиваний:
0
Добавлен:
27.12.2025
Размер:
546.57 Кб
Скачать

МИНОБРНАУКИ РОССИИ САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ ЭЛЕКТРОТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ «ЛЭТИ» ИМ. В.И. УЛЬЯНОВА (ЛЕНИНА) Кафедра САПР

ОТЧЕТ по лабораторной работе №2

по дисциплине «Архитектура параллельных вычислительных систем» ТЕМА: «Сортировка на системах с общей памятью»

Вариант 3

Студенты гр. 1302

Харитонов А.А.

 

Наволоцкий И.Р.

 

Солончак И.П.

Преподаватель

Костичев С.В.

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

2025

Цель работы

Изучить

организацию

параллельных

вычислений

для

не

вычислительных задач на системах с общей памятью.

1.Задание на лабораторную работу

1.1. Разработать алгоритм сортировки выбором (SelectSort) для последовательных и параллельных вычислений

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

1.3.Запустить программы для следующих значений размерности цепочек данных: 10, 100, 500, 1000, 5000, 10000.

1.4.Оценить размерность цепочек данных, при которых эффективнее использовать алгоритмы последовательного и параллельного вычислений для разного числа потоков (по крайней мере, для меньшего, равного и большего, чем число процессоров)

2. Программное и аппаратное окружение при выполнении работы

При выполнении лабораторной работы использовался ноутбук Apple MacBook Air с чипом M1, содержащим 8-ядерный CPU и 7-ядерный GPU, а также 8 Гб оперативной памяти. Работа выполнялась под управлением macOS Sequoia 15.7.1.

Среда разработки: Visual Studio Code 1.105.1, система сборки: CMake, управляемый через расширение CMake Tools в VS Code.

3.Формализация задачи

1.Запуск управляющего скрипта 1302_3_2.sh.

2.Скрипт компилирует исходный код 1302_3_2.cpp в исполняемый файл.

2

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

Исходный и отсортированный (методом sequentialSelectionSort) массивы из 10 целых чисел выводятся на экран.

4.Начало основного тестирования. Скрипт запускает внешний цикл, который перебирает заданное количество потоков для параллельных вычислений: {2, 4, 6, 8, 10, 12, 14, 16}.

5.Внутренний цикл по размерам данных. На каждой итерации внешнего цикла (для каждого количества потоков) запускается C++ программа. Эта программа, в свою очередь, выполняет внутренний цикл по предопределенным размерам массивов: {10, 100, 500, 1000, 5000, 10000}.

6.Для каждого размера N из списка внутри C++ программы выполняются следующие шаги:

a.Создается вектор arr размера N, который заполняется случайными целыми числами.

b.Замер времени выполнения последовательной сортировки выбором:

Создается копия исходного вектора arr.

Засекается начальное время.

Вызывается функция sequentialSelectionSort для сортировки копии.

Вычисляется и сохраняется итоговое время выполнения.

c.Замер времени выполнения параллельной MPI сортировки выбором:

Все процессы синхронизируются.

Главный процесс (ранг 0) засекает начальное

время.

3

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

После завершения сортировки все процессы снова синхронизируются.

Вычисляется и сохраняется итоговое время выполнения.

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

e.Отсортированный параллельным методом массив дописывается в файл sorted_results.txt. Результаты для разных размеров разделяются в файле пустой строкой.

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

4.Примеры работы программы

Исходный код программ приведён в приложении А. Блок-схемы алгоритмов приведены в приложении Б.

4.1.Демонстрация работы программы

При запуске управляющий скрипт сначала выполняет программу в демонстрационном режиме для сортировки небольшого массива из 10 элементов. Как видно на рисунке 1, программа выводит на экран исходный, случайно сгенерированный массив (initial array), а затем результат его обработки – полностью отсортированный массив (sorted array). Этот шаг служит для визуального подтверждения корректности реализации алгоритма

4

сортировки выбором перед переходом к масштабным замерам производительности.

Рисунок 1 – Пример работы программы при сортировке 10 элементов

4.2.Анализ результатов производительности

После верификации программа выполняет серию автоматизированных тестов для измерения времени сортировки массивов разных размеров (от 10 до 10000 элементов) с использованием последовательного и параллельного (на базе MPI) алгоритмов. Результаты замеров для разного числа потоков сведены в общую таблицу, представленную на рисунке 2.

Рисунок 2 – Итоговая сводная таблица производительности для последовательной и параллельной сортировки выбором

4.2.1. Влияние размера массива на время выполнения

Сортировка выбором имеет теоретическую временную сложность O(n²), где n – количество элементов в массиве. Полученные экспериментальные

5

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

При увеличении размера в 10 раз (от 1000 до 10000 элементов) время выполнения выросло примерно в 95 раз (с 1.426 мс до 135.115 мс), что очень близко к теоретическому росту в 100 раз (10²).

При увеличении размера в 2 раза (от 5000 до 10000 элементов) время выполнения выросло почти в 3.4 раза (с 39.868 мс до 135.115 мс), что также соотносится с теоретическим ростом в 4 раза (2²).

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

4.2.2. Эффективность параллельных вычислений

Сравнение последовательного и параллельного (MPI) методов показывает, что эффективность распараллеливания сильно зависит от размера задачи.

На малых размерах (10, 100, 500, 1000 элементов) параллельный алгоритм работает значительно медленнее последовательного. Например, для 1000 элементов последовательная версия занимает 1.426 мс, в то время как параллельная на 2 потоках – 1.492 мс. Это объясняется высокими накладными расходами на MPI-коммуникации (функции MPI_Bcast, MPI_Reduce), которые на малых задачах «съедают» весь потенциальный выигрыш от распараллеливания вычислений.

На больших размерах (5000 и 10000 элементов): начиная с размера 5000, параллельная версия становится эффективнее.

o Для 5000 элементов наилучший результат (24.727 мс на 2 потоках) оказался примерно в 1.6 раза быстрее последовательной версии (39.868 мс).

o Для 10000 элементов преимущество становится еще более очевидным: наилучший параллельный результат (82.212 мс на 4

6

потоках) почти в 1.65 раза быстрее последовательной версии (135.115 мс).

Это демонстрирует, что для данного MPI-алгоритма существует точка безубыточности (в данном случае, между 1000 и 5000 элементов), после которой параллельные вычисления становятся выгодными.

4.2.3. Влияние количества потоков и масштабируемость

Анализ времени выполнения MPI-алгоритма при разном количестве потоков (процессов) выявляет сложную и нелинейную зависимость.

Хорошая масштабируемость (для больших N): на размере 10000 элементов наблюдается прирост производительности при увеличении числа потоков с 2 до 4 (время сократилось с 83.145 мс до 82.212 мс).

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

Для 5000 элементов оптимальным является использование 2 потоков. Уже 4 потока работают медленнее, а 16 – в 23 раза медленнее (580 мс против

24.7мс).

Для 10000 элементов оптимальное число потоков – 4. При 16 потоках время выполнения возрастает до 1287 мс, что в 15 раз медленнее, чем при 4 потоках, и в 9.5 раз медленнее, чем последовательная версия.

Это явление, характерное для MPI-программ на системах с общей памятью (один компьютер), связано с доминирующей ролью коммуникационных издержек:

Стоимость MPI_Bcast и MPI_Reduce: алгоритм на каждой из N- 1 итераций требует синхронизации и сбора данных (Reduce), а затем рассылки обновленного массива всем процессам (Bcast). Чем больше процессов участвует, тем больше времени уходит на эти операции обмена данными.

Соотношение вычислений и коммуникаций: в сортировке выбором объем вычислений на каждом шаге (поиск минимума) относительно

7

мал. Оказывается, что выигрыш от распараллеливания этого поиска на большом числе ядер полностью нивелируется огромными затратами времени на постоянную синхронизацию всех этих ядер между собой. Процессы больше времени тратят на «общение» друг с другом, чем на полезную работу.

Таким образом, для данной реализации оптимальным является использование небольшого числа потоков (2-4), и только на массивах большого размера.

Вывод

В ходе лабораторной работы были реализованы и сравнены последовательный и параллельный (MPI) алгоритмы сортировки выбором. Анализ показал, что параллельная версия эффективна только на больших массивах (от 5000 элементов), где было достигнуто ускорение до 1.65 раза. На малых данных производительность снижается из-за высоких накладных расходов на коммуникации MPI.

Было установлено, что производительность плохо масштабируется с ростом числа потоков. Оптимальное ускорение достигалось на 2-4 потоках, а дальнейшее их увеличение приводило к резкой деградации из-за коммуникационного барьера: частые глобальные синхронизации на каждой итерации нивелировали выигрыш от параллельных вычислений.

Таким образом, работа подтвердила, что для алгоритмов с частыми синхронизациями, как у сортировки выбором, эффективность MPI критически зависит от минимизации коммуникационных издержек, которые могут перевесить пользу от распараллеливания.

8

ПРИЛОЖЕНИЕ А Листинги исходного кода программы

1302_3_2.cpp

#include <iostream> #include <vector> #include <random> #include <chrono> #include <string> #include <algorithm> #include <limits> #include <iomanip> #include <fstream> #include <mpi.h>

std::vector<int> generateData(int size) { std::vector<int> data(size); std::random_device rd;

std::mt19937 gen(rd()); std::uniform_int_distribution<> distrib(1, 1000); for (int i = 0; i < size; ++i) {

data[i] = distrib(gen);

}

return data;

}

void printArray(const std::vector<int>& arr, const std::string& label) { std::cout << label << ":" << std::endl;

for (int val : arr) { std::cout << val << " ";

}

9

std::cout << std::endl;

}

void sequentialSelectionSort(std::vector<int>& arr) { int n = arr.size();

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

for (int j = i + 1; j < n; ++j) { if (arr[j] < arr[minIdx]) {

minIdx = j;

}

}

if (minIdx != i) { std::swap(arr[i], arr[minIdx]);

}

}

}

void parallelSelectionSort(std::vector<int>& arr, int worldRank, int worldSize) { int n = arr.size();

MPI_Bcast(arr.data(), n, MPI_INT, 0, MPI_COMM_WORLD); struct { int value; int index; } localMin, globalMin;

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

localMin.value = std::numeric_limits<int>::max(); localMin.index = -1;

int elementsToSearch = n - i;

int chunkSize = elementsToSearch / worldSize; int remainder = elementsToSearch % worldSize;

int startIdx = i + worldRank * chunkSize + std::min(worldRank, remainder); int endIdx = startIdx + chunkSize + (worldRank < remainder ? 1 : 0);

for (int j = startIdx; j < endIdx; ++j) { if (arr[j] < localMin.value) {

localMin.value = arr[j];

10