Skip to content

Commit e89ed25

Browse files
authored
Никитина Валерия. Технология SEQ-MPI. Быстрая сортировка с простым слиянием. Вариант 14 (#138)
<!-- Требования к названию pull request: "<Фамилия> <Имя>. Технология <TECHNOLOGY_NAME:SEQ|OMP|TBB|STL|MPI>. <Полное название задачи>. Вариант <Номер>" --> ## Описание <!-- Пожалуйста, предоставьте подробное описание вашей реализации, включая: - основные детали решения (описание выбранного алгоритма) - применение технологии параллелизма (если применимо) --> - **Задача**: Быстрая сортировка с простым слиянием - **Вариант**: 14 - **Технология**: SEQ, MPI - **Описание**: Реализован алгоритм быстрой сортировки с простым слиянием в двух вариантах. Параллельная MPI-версия построена на стратегии распределенной обработки: декомпозиция данных выполняется через MPI_Scatterv (с учетом неравномерного распределения остатка), локальная сортировка частей происходит независимо, а консолидация результатов — через MPI_Gatherv с последующим итеративным слиянием (std::inplace_merge) на корневом узле. Последовательная версия (SEQ) выполняет прямую сортировку входного массива средствами стандартной библиотеки (std::ranges::sort), исключая накладные расходы на межпроцессное взаимодействие. --- ## Чек-лист <!-- Пожалуйста, убедитесь, что следующие пункты выполнены **до** отправки pull request'а и запроса его ревью: --> - [x] **Статус CI**: Все CI-задачи (сборка, тесты, генерация отчёта) успешно проходят на моей ветке в моем форке - [x] **Директория и именование задачи**: Я создал директорию с именем `<фамилия>_<первая_буква_имени>_<короткое_название_задачи>` - [x] **Полное описание задачи**: Я предоставил полное описание задачи в теле pull request - [x] **clang-format**: Мои изменения успешно проходят `clang-format` локально в моем форке (нет ошибок форматирования) - [x] **clang-tidy**: Мои изменения успешно проходят `clang-tidy` локально в моем форке (нет предупреждений/ошибок) - [x] **Функциональные тесты**: Все функциональные тесты успешно проходят локально на моей машине - [x] **Тесты производительности**: Все тесты производительности успешно проходят локально на моей машине - [x] **Ветка**: Я работаю в ветке, названной точно так же, как директория моей задачи (например, `nesterov_a_vector_sum`), а не в `master` - [x] **Правдивое содержание**: Я подтверждаю, что все сведения, указанные в этом pull request, являются точными и достоверными <!-- ПРИМЕЧАНИЕ: Ложные сведения в этом чек-листе могут привести к отклонению PR и получению нулевого балла за соответствующую задачу. -->
1 parent 6026c2b commit e89ed25

10 files changed

Lines changed: 514 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <utility>
5+
#include <vector>
6+
7+
#include "task/include/task.hpp"
8+
9+
namespace nikitina_v_quick_sort_merge {
10+
11+
using InType = std::vector<int>;
12+
using OutType = std::vector<int>;
13+
using BaseTask = ppc::task::Task<InType, OutType>;
14+
15+
inline std::pair<int, int> Partition(std::vector<int> &vec, int left, int right) {
16+
int i = left;
17+
int j = right;
18+
int pivot = vec[(left + right) / 2];
19+
20+
while (i <= j) {
21+
while (vec[i] < pivot) {
22+
i++;
23+
}
24+
while (vec[j] > pivot) {
25+
j--;
26+
}
27+
if (i <= j) {
28+
std::swap(vec[i], vec[j]);
29+
i++;
30+
j--;
31+
}
32+
}
33+
return {i, j};
34+
}
35+
36+
inline void QuickSortImpl(std::vector<int> &vec, int left, int right) {
37+
if (left >= right) {
38+
return;
39+
}
40+
41+
std::vector<std::pair<int, int>> stack;
42+
stack.reserve(static_cast<size_t>(right - left) + 1);
43+
stack.emplace_back(left, right);
44+
45+
while (!stack.empty()) {
46+
auto [l, r] = stack.back();
47+
stack.pop_back();
48+
49+
auto [i, j] = Partition(vec, l, r);
50+
51+
if (l < j) {
52+
stack.emplace_back(l, j);
53+
}
54+
if (i < r) {
55+
stack.emplace_back(i, r);
56+
}
57+
}
58+
}
59+
60+
} // namespace nikitina_v_quick_sort_merge
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"student": {
3+
"first_name": "Валерия",
4+
"last_name": "Никитина",
5+
"middle_name": "Владимировна",
6+
"group_number": "3823Б1ФИ2",
7+
"task_number": "3"
8+
}
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "nikitina_v_quick_sort_merge/common/include/common.hpp"
4+
#include "task/include/task.hpp"
5+
6+
namespace nikitina_v_quick_sort_merge {
7+
8+
class TestTaskMPI : public BaseTask {
9+
public:
10+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
11+
return ppc::task::TypeOfTask::kMPI;
12+
}
13+
explicit TestTaskMPI(const InType &in);
14+
15+
private:
16+
bool ValidationImpl() override;
17+
bool PreProcessingImpl() override;
18+
bool RunImpl() override;
19+
bool PostProcessingImpl() override;
20+
};
21+
22+
} // namespace nikitina_v_quick_sort_merge
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#include "nikitina_v_quick_sort_merge/mpi/include/ops_mpi.hpp"
2+
3+
#include <mpi.h>
4+
5+
#include <algorithm>
6+
#include <vector>
7+
8+
#include "nikitina_v_quick_sort_merge/common/include/common.hpp"
9+
10+
namespace nikitina_v_quick_sort_merge {
11+
12+
TestTaskMPI::TestTaskMPI(const InType &in) {
13+
SetTypeOfTask(GetStaticTypeOfTask());
14+
GetInput() = in;
15+
}
16+
17+
bool TestTaskMPI::ValidationImpl() {
18+
return true;
19+
}
20+
21+
bool TestTaskMPI::PreProcessingImpl() {
22+
return true;
23+
}
24+
25+
bool TestTaskMPI::RunImpl() {
26+
int size = 0;
27+
int rank = 0;
28+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
29+
MPI_Comm_size(MPI_COMM_WORLD, &size);
30+
31+
int total_elements = 0;
32+
if (rank == 0) {
33+
total_elements = static_cast<int>(GetInput().size());
34+
}
35+
36+
MPI_Bcast(&total_elements, 1, MPI_INT, 0, MPI_COMM_WORLD);
37+
38+
if (total_elements == 0) {
39+
return true;
40+
}
41+
42+
std::vector<int> send_counts(size);
43+
std::vector<int> displs(size);
44+
45+
int base_count = total_elements / size;
46+
int remainder = total_elements % size;
47+
48+
int current_displ = 0;
49+
for (int i = 0; i < size; ++i) {
50+
send_counts[i] = base_count + (i < remainder ? 1 : 0);
51+
displs[i] = current_displ;
52+
current_displ += send_counts[i];
53+
}
54+
55+
std::vector<int> local_vec(send_counts[rank]);
56+
57+
MPI_Scatterv(rank == 0 ? GetInput().data() : nullptr, send_counts.data(), displs.data(), MPI_INT, local_vec.data(),
58+
send_counts[rank], MPI_INT, 0, MPI_COMM_WORLD);
59+
60+
if (!local_vec.empty()) {
61+
QuickSortImpl(local_vec, 0, static_cast<int>(local_vec.size()) - 1);
62+
}
63+
64+
if (rank == 0) {
65+
GetOutput().resize(total_elements);
66+
}
67+
68+
MPI_Gatherv(local_vec.data(), send_counts[rank], MPI_INT, rank == 0 ? GetOutput().data() : nullptr,
69+
send_counts.data(), displs.data(), MPI_INT, 0, MPI_COMM_WORLD);
70+
71+
if (rank == 0) {
72+
auto current_end = GetOutput().begin() + send_counts[0];
73+
74+
for (int i = 1; i < size; ++i) {
75+
if (send_counts[i] > 0) {
76+
auto next_end = current_end + send_counts[i];
77+
std::inplace_merge(GetOutput().begin(), current_end, next_end);
78+
current_end = next_end;
79+
}
80+
}
81+
}
82+
83+
return true;
84+
}
85+
86+
bool TestTaskMPI::PostProcessingImpl() {
87+
return true;
88+
}
89+
90+
} // namespace nikitina_v_quick_sort_merge
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Отчет: Быстрая сортировка с простым слиянием
2+
3+
- **Студент:** Никитина Валерия Владимировна
4+
- **Группа:** 3823Б1ФИ2
5+
- **Технология:** MPI, SEQ
6+
- **Вариант:** 14
7+
8+
## 1. Введение
9+
10+
Сортировка больших массивов данных — классическая задача, требовательная к вычислительным ресурсам. С увеличением объема данных время работы последовательных алгоритмов ($O(N \log N)$) становится критичным. Параллельные вычисления позволяют разделить задачу на подзадачи, выполняемые одновременно на разных вычислительных узлах.
11+
12+
В данной работе реализован параллельный алгоритм сортировки, использующий стратегию «разделяй и властвуй». Данные распределяются между процессами, сортируются локально, а затем собираются и сливаются в итоговый массив.
13+
14+
## 2. Постановка задачи
15+
16+
**Цель:** Разработать MPI-приложение для сортировки целочисленного вектора.
17+
**Входные данные:** Вектор `std::vector<int>`, доступный на корневом процессе.
18+
**Выходные данные:** Отсортированный вектор на корневом процессе.
19+
20+
**Требования:**
21+
1. Реализовать алгоритм быстрой сортировки (QuickSort) вручную, не используя стандартные библиотечные функции сортировки.
22+
2. Реализовать параллельную версию (MPI) с использованием операций `Scatterv`, локальной сортировки и `Gatherv` с последующим слиянием.
23+
3. Обеспечить корректность на любых размерах данных.
24+
25+
## 3. Описание алгоритмов
26+
27+
Для обеспечения честного сравнения и в SEQ, и в MPI версиях используется **одинаковая реализация** алгоритма Хоара (QuickSort), написанная вручную.
28+
29+
### 3.1. Реализация QuickSort (Ядро)
30+
Используется классическая схема с рекурсией:
31+
* Выбирается опорный элемент (pivot) из середины массива.
32+
* Массив разделяется на две части: слева элементы меньше pivot, справа — больше.
33+
* Алгоритм рекурсивно вызывается для левой и правой частей.
34+
* Сложность в среднем: $O(N \log N)$.
35+
36+
### 3.2. Последовательный алгоритм (SEQ)
37+
Последовательная версия выполняет функцию `QuickSortImpl` на всем входном массиве в рамках одного процесса. Это служит базой для измерения "чистого" времени вычисления без накладных расходов на сеть.
38+
39+
### 3.3. Параллельный алгоритм (MPI)
40+
Алгоритм состоит из четырех этапов:
41+
42+
1. **Распределение (Scatter):**
43+
* Корневой процесс (Rank 0) делит входной массив размером $N$ на $P$ частей.
44+
* Размер части для процесса $i$: $count_i = N/P + (i < N\%P ? 1 : 0)$.
45+
* Используется `MPI_Scatterv` для рассылки блоков разного размера.
46+
47+
2. **Локальная сортировка (Compute):**
48+
* Каждый процесс (включая Root) запускает `QuickSortImpl` для полученного локального буфера.
49+
* Этот этап выполняется полностью параллельно.
50+
51+
3. **Сбор данных (Gather):**
52+
* Отсортированные локальные части собираются обратно на корневой процесс через `MPI_Gatherv`.
53+
* На этом этапе массив на Root состоит из $P$ отсортированных кусков, идущих подряд.
54+
55+
4. **Слияние (Merge):**
56+
* Корневой процесс выполняет слияние полученных частей в один отсортированный массив.
57+
* Используется функция `std::inplace_merge`, применяемая последовательно к границам собранных блоков.
58+
59+
## 4. Экспериментальная часть
60+
61+
### 4.1. Условия эксперимента
62+
* **Платформа:** Docker-контейнер на локальной машине (4 физических ядра).
63+
* **Компилятор:** GCC 14.2.0.
64+
* **Данные:** Вектор размером **1,000,000** элементов (`int`), случайное заполнение.
65+
66+
### 4.2. Результаты измерений
67+
Замеры времени выполнения (среднее по 5 запускам):
68+
69+
| Число процессов (P) | Время выполнения (сек) | Ускорение ($S$) | Эффективность ($E$) |
70+
| :---: | :---: | :---: | :---: |
71+
| **SEQ (1)** | **0.082** | 1.00 | 100% |
72+
| **MPI (1)** | 0.085 | 0.96 | 96% |
73+
| **MPI (2)** | 0.051 | 1.60 | 80% |
74+
| **MPI (3)** | 0.046 | 1.78 | 59% |
75+
| **MPI (4)** | 0.048 | 1.70 | 42% |
76+
| **MPI (8)** | 0.065 | 1.26 | 15% |
77+
78+
### 4.3. Анализ производительности
79+
1. **SEQ vs MPI(1):** Время практически идентично, небольшое замедление MPI(1) обусловлено инициализацией буферов и лишним копированием памяти при `Scatter/Gather` внутри одного процесса.
80+
2. **Масштабируемость:**
81+
* На 2 и 3 процессах наблюдается хорошее ускорение (до 1.78x). Параллельная сортировка частей перекрывает затраты на коммуникацию.
82+
* На 4 и более процессах рост производительности останавливается. Это связано с тем, что этап **Слияния (Merge)** выполняется последовательно на одном узле. Чем больше процессов, тем больше частей нужно слить, и сложность этого этапа начинает доминировать над выигрышем от сортировки.
83+
* Также влияет ограничение физических ядер (тесты запускались на 4 ядрах), что при $P > 4$ вызывает конкуренцию за процессорное время.
84+
85+
## 5. Выводы
86+
87+
В ходе работы была реализована параллельная версия алгоритма QuickSort.
88+
89+
1. **Корректность:** Реализована собственная функция сортировки, которая успешно интегрирована как в SEQ, так и в MPI версии. Тесты подтверждают правильность работы на любых входных данных.
90+
2. **Эффективность:** Алгоритм показал прирост производительности на малом числе процессов.
91+
3. **Архитектура:** Использование схемы `Scatter -> Local Sort -> Gather -> Merge` является эффективным решением для распределенной памяти, однако фаза последовательного слияния является узким местом (bottleneck), ограничивающим бесконечное масштабирование.
92+
93+
## 6. Список литературы
94+
1. MPI Forum. MPI: A Message-Passing Interface Standard.
95+
2. Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. Алгоритмы: построение и анализ.
96+
3. Документация C++ Reference (std::sort, std::inplace_merge).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "nikitina_v_quick_sort_merge/common/include/common.hpp"
4+
#include "task/include/task.hpp"
5+
6+
namespace nikitina_v_quick_sort_merge {
7+
8+
class TestTaskSEQ : public BaseTask {
9+
public:
10+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
11+
return ppc::task::TypeOfTask::kSEQ;
12+
}
13+
explicit TestTaskSEQ(const InType &in);
14+
15+
private:
16+
bool ValidationImpl() override;
17+
bool PreProcessingImpl() override;
18+
bool RunImpl() override;
19+
bool PostProcessingImpl() override;
20+
};
21+
22+
} // namespace nikitina_v_quick_sort_merge
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include "nikitina_v_quick_sort_merge/seq/include/ops_seq.hpp"
2+
3+
#include <vector>
4+
5+
#include "nikitina_v_quick_sort_merge/common/include/common.hpp"
6+
7+
namespace nikitina_v_quick_sort_merge {
8+
9+
TestTaskSEQ::TestTaskSEQ(const InType &in) {
10+
SetTypeOfTask(GetStaticTypeOfTask());
11+
GetInput() = in;
12+
}
13+
14+
bool TestTaskSEQ::ValidationImpl() {
15+
return true;
16+
}
17+
18+
bool TestTaskSEQ::PreProcessingImpl() {
19+
GetOutput() = GetInput();
20+
return true;
21+
}
22+
23+
bool TestTaskSEQ::RunImpl() {
24+
if (GetOutput().empty()) {
25+
return true;
26+
}
27+
QuickSortImpl(GetOutput(), 0, static_cast<int>(GetOutput().size()) - 1);
28+
return true;
29+
}
30+
31+
bool TestTaskSEQ::PostProcessingImpl() {
32+
return true;
33+
}
34+
35+
} // namespace nikitina_v_quick_sort_merge
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"tasks_type": "processes",
3+
"tasks": {
4+
"mpi": "enabled",
5+
"seq": "enabled"
6+
}
7+
}

0 commit comments

Comments
 (0)