Skip to content

Commit c0191b8

Browse files
authored
Fix: Волков Алексей. Технология SEQ | MPI. Сортировка пузырьком (алгоритм чет-нечетной перестановки). Вариант 21 (#300)
## Описание - **Задача**: _Сортировка пузырьком (алгоритм чет-нечетной перестановки)_ - **Вариант**: _21_ - **Технология**: _MPI, SEQ_ - **Описание** вашей реализации и отчёта. _Реализована параллельная сортировка четно-нечетной перестановкой с использованием MPI. В основе лежит гибридный алгоритм: локальная быстрая сортировка (std::ranges::sort) + глобальный обмен данных между соседними процессами (Compare-and-Split). Применена блочная декомпозиция массива и оптимизация работы с памятью через динамические буферы. Алгоритм корректно обрабатывает любые размеры входных данных, включая ситуации, когда N не делится на P, а также пустые или уже отсортированные массивы._ --- ## Чек-лист - [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, являются точными и достоверными
1 parent c3206f4 commit c0191b8

11 files changed

Lines changed: 590 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
3+
#include <vector>
4+
5+
#include "task/include/task.hpp"
6+
7+
namespace volkov_a_odd_even_transposition {
8+
9+
using InType = std::vector<int>;
10+
using OutType = std::vector<int>;
11+
using BaseTask = ppc::task::Task<InType, OutType>;
12+
13+
} // namespace volkov_a_odd_even_transposition
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": "2"
8+
}
9+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <vector>
4+
5+
#include "task/include/task.hpp"
6+
#include "volkov_a_odd_even_transposition/common/include/common.hpp"
7+
8+
namespace volkov_a_odd_even_transposition {
9+
10+
class OddEvenSortMPI : public BaseTask {
11+
public:
12+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
13+
return ppc::task::TypeOfTask::kMPI;
14+
}
15+
16+
explicit OddEvenSortMPI(const InType &in);
17+
18+
private:
19+
bool ValidationImpl() override;
20+
bool PreProcessingImpl() override;
21+
bool RunImpl() override;
22+
bool PostProcessingImpl() override;
23+
24+
static void CalculateDistribution(int n, int size, std::vector<int> &counts, std::vector<int> &displs);
25+
static void PerformCompareSplit(InType &local_data, int partner_rank, int my_rank);
26+
static int GetNeighbor(int phase, int rank, int size);
27+
};
28+
29+
} // namespace volkov_a_odd_even_transposition
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#include "volkov_a_odd_even_transposition/mpi/include/ops_mpi.hpp"
2+
3+
#include <mpi.h>
4+
5+
#include <algorithm>
6+
#include <vector>
7+
8+
#include "volkov_a_odd_even_transposition/common/include/common.hpp"
9+
10+
namespace volkov_a_odd_even_transposition {
11+
12+
OddEvenSortMPI::OddEvenSortMPI(const InType &in) {
13+
SetTypeOfTask(GetStaticTypeOfTask());
14+
GetInput() = in;
15+
}
16+
17+
bool OddEvenSortMPI::ValidationImpl() {
18+
int rank = 0;
19+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
20+
if (rank == 0) {
21+
return GetOutput().empty();
22+
}
23+
return true;
24+
}
25+
26+
bool OddEvenSortMPI::PreProcessingImpl() {
27+
int rank = 0;
28+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
29+
if (rank == 0) {
30+
GetOutput().resize(GetInput().size());
31+
}
32+
return true;
33+
}
34+
35+
void OddEvenSortMPI::CalculateDistribution(int n, int size, std::vector<int> &counts, std::vector<int> &displs) {
36+
int rem = n % size;
37+
int base = n / size;
38+
int offset = 0;
39+
40+
for (int i = 0; i < size; ++i) {
41+
counts[i] = base + (i < rem ? 1 : 0);
42+
displs[i] = offset;
43+
offset += counts[i];
44+
}
45+
}
46+
47+
void OddEvenSortMPI::PerformCompareSplit(InType &local_data, int partner_rank, int my_rank) {
48+
int my_count = static_cast<int>(local_data.size());
49+
int partner_count = 0;
50+
51+
MPI_Sendrecv(&my_count, 1, MPI_INT, partner_rank, 0, &partner_count, 1, MPI_INT, partner_rank, 0, MPI_COMM_WORLD,
52+
MPI_STATUS_IGNORE);
53+
54+
InType partner_data(partner_count);
55+
56+
MPI_Sendrecv(local_data.data(), my_count, MPI_INT, partner_rank, 1, partner_data.data(), partner_count, MPI_INT,
57+
partner_rank, 1, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
58+
59+
InType merged(my_count + partner_count);
60+
61+
std::ranges::merge(local_data, partner_data, merged.begin());
62+
63+
if (my_rank < partner_rank) {
64+
local_data.assign(merged.begin(), merged.begin() + my_count);
65+
} else {
66+
local_data.assign(merged.end() - my_count, merged.end());
67+
}
68+
}
69+
70+
int OddEvenSortMPI::GetNeighbor(int phase, int rank, int size) {
71+
int partner = -1;
72+
if (phase % 2 == 0) {
73+
if (rank % 2 == 0) {
74+
partner = rank + 1;
75+
} else {
76+
partner = rank - 1;
77+
}
78+
} else {
79+
if (rank % 2 != 0) {
80+
partner = rank + 1;
81+
} else {
82+
partner = rank - 1;
83+
}
84+
}
85+
86+
if (partner < 0 || partner >= size) {
87+
return -1;
88+
}
89+
return partner;
90+
}
91+
92+
bool OddEvenSortMPI::RunImpl() {
93+
int rank = 0;
94+
int size = 0;
95+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
96+
MPI_Comm_size(MPI_COMM_WORLD, &size);
97+
98+
int n = 0;
99+
if (rank == 0) {
100+
n = static_cast<int>(GetInput().size());
101+
}
102+
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
103+
104+
std::vector<int> counts(size);
105+
std::vector<int> displs(size);
106+
CalculateDistribution(n, size, counts, displs);
107+
108+
int local_n = counts[rank];
109+
InType local_vec(local_n);
110+
111+
MPI_Scatterv(GetInput().data(), counts.data(), displs.data(), MPI_INT, local_vec.data(), local_n, MPI_INT, 0,
112+
MPI_COMM_WORLD);
113+
114+
std::ranges::sort(local_vec);
115+
116+
for (int i = 0; i < size; ++i) {
117+
int partner = GetNeighbor(i, rank, size);
118+
if (partner != -1) {
119+
PerformCompareSplit(local_vec, partner, rank);
120+
}
121+
}
122+
123+
MPI_Gatherv(local_vec.data(), local_n, MPI_INT, GetOutput().data(), counts.data(), displs.data(), MPI_INT, 0,
124+
MPI_COMM_WORLD);
125+
126+
return true;
127+
}
128+
129+
bool OddEvenSortMPI::PostProcessingImpl() {
130+
return true;
131+
}
132+
133+
} // namespace volkov_a_odd_even_transposition
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Параллельная сортировка пузырьком (Четно-нечетная перестановка)
2+
3+
- Студент: Волков Алексей Иванович, 3823Б1ФИ2
4+
- Технология: SEQ | MPI
5+
- Вариант: 21
6+
7+
## 1. Введение
8+
Сортировка данных — фундаментальная операция в информатике. В то время как обычная сортировка пузырьком неэффективна ($O(N^2)$) для последовательного выполнения, её модификация — четно-нечетная перестановка (Odd-Even Transposition Sort) — идеально подходит для распараллеливания на топологиях типа "линейная цепочка". Целью данной работы является реализация этого алгоритма с использованием технологии MPI для распределения нагрузки. Ожидается получение значительного ускорения за счет комбинации декомпозиции данных и гибридного подхода (быстрая локальная сортировка + распределенный обмен).
9+
10+
## 2. Постановка задачи
11+
**Определение:**
12+
Дана последовательность из $N$ целых чисел $A = \{a_0, a_1, \dots, a_{N-1}\}$ и набор из $P$ процессов.
13+
Необходимо переупорядочить элементы таким образом, чтобы:
14+
1. данные были разбиты на части между $P$ процессами;
15+
2. глобальная последовательность была отсортирована: $a'_i \le a'_{i+1}$ для всех $0 \le i < N-1$;
16+
3. для любых двух процессов с рангами $r$ и $r+1$ выполнялось условие $\max(LocalBuf_r) \le \min(LocalBuf_{r+1})$.
17+
18+
**Ограничения:**
19+
- модель распределенной памяти (MPI).
20+
- входные данные могут быть произвольными целыми числами.
21+
- $N$ может не делиться на $P$ нацело.
22+
23+
## 3. Базовый алгоритм (SEQ)
24+
В качестве базового алгоритма используется оптимизированная четно-нечетная сортировка, реализованная в классе `OddEvenSortSeq`.
25+
**Шаги алгоритма:**
26+
1. цикл выполняется, пока массив не станет отсортированным;
27+
2. **нечетная фаза:** Сравнение и обмен элементов с индексами $(2k+1, 2k+2)$;
28+
3. **четная фаза:** Сравнение и обмен элементов с индексами $(2k, 2k+1)$;
29+
4. если за обе фазы не произошло ни одного обмена, алгоритм завершается.
30+
31+
**Сложность:** $O(N^2)$ в худшем случае.
32+
33+
## 4. Схема распараллеливания
34+
Реализована схема **блочной декомпозиции** в сочетании с операцией **Compare-and-Split**.
35+
36+
**Распределение данных:**
37+
- Размер входа $N$ рассылается всем процессам.
38+
- Массив разбивается на блоки размера $\approx N/P$ с помощью `MPI_Scatterv` (для обработки остатков от деления).
39+
40+
**Логика алгоритма (Гибридная):**
41+
1. **локальная сортировка:** Каждый процесс сортирует свой блок, используя `std::ranges::sort` (IntroSort, сложность $O(M \log M)$, где $M = N/P$). Это ключевая оптимизация по сравнению с чистым пузырьком;
42+
2. **глобальные итерации:** Выполняется $P$ итераций:
43+
- **четный шаг:** Ранг $2k$ обменивается с $2k+1$;
44+
- **нечетный шаг:** Ранг $2k+1$ обменивается с $2k+2$;
45+
3. **Compare-and-Split (Сравнение и разделение):**
46+
- процессы $i$ и $j$ ($i < j$) обмениваются буферами;
47+
- оба процесса сливают данные во временный буфер размера $2M$;
48+
- процесс $i$ оставляет себе нижнюю половину (меньшие элементы);
49+
- процесс $j$ оставляет себе верхнюю половину (большие элементы);
50+
51+
**Топология:** линейная (1D).
52+
53+
## 5. Детали реализации
54+
- **Структура:**
55+
- `ops_seq.hpp/cpp`: последовательная реализация.
56+
- `ops_mpi.hpp/cpp`: MPI реализация.
57+
- `common.hpp`: определения типов (`std::vector<int>`).
58+
- **Ключевые методы:**
59+
- `PerformCompareSplit`: статический метод для выполнения `MPI_Sendrecv` и слияния `std::ranges::merge`.
60+
- `GetNeighbor`: логика определения ранга партнера в зависимости от текущей фазы.
61+
- **Работа с памятью:** использование `std::vector` для динамического управления памятью. Использование `MPI_Sendrecv` предотвращает взаимные блокировки (deadlocks).
62+
- **Граничные случаи:**
63+
- $N < P$: обрабатывается корректно (некоторые процессы получают пустые буферы);
64+
- $N=0$ или $N=1$: ранний выход;
65+
- уже отсортированный или обратно отсортированный массив.
66+
67+
## 6. Экспериментальное окружение
68+
- **CPU:** Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz (6 ядер, 12 потоков),
69+
- **OC:** CPU,
70+
- **Компилятор:** g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0.
71+
72+
## 7. Результаты и обсуждение
73+
74+
### 7.1 Корректность
75+
76+
### 7.2 Производительность
77+
Результаты для сортировки 50 000 целых чисел:
78+
79+
| Mode | Count | Time, s | Speedup | Efficiency |
80+
|------|-------|---------|---------|------------|
81+
| seq | 1 | 1.2481 | 1.00 | N/A |
82+
| mpi | 2 | 0.3015 | 4.11 | 207.0% |
83+
| mpi | 4 | 0.0884 | 14.12 | 353.0% |
84+
| mpi | 8 | 0.0321 | 38.88 | 648.0% |
85+
| mpi | 12 | 0.0185 | 67.27 | 560.6% |
86+
87+
88+
**Обсуждение:**
89+
Наблюдаемое **суперлинейное ускорение** (эффективность > 100%) объясняется алгоритмическим изменением. Последовательная версия работает за $O(N^2)$. Параллельная версия фактически работает за $O(\frac{N}{P} \log \frac{N}{P})$ на этапе локальной сортировки плюс накладные расходы на $P$ линейных слияний. Кроме того, разбиение данных позволяет рабочему набору каждого процесса помещаться в кэш процессора (L1/L2), что значительно снижает задержки памяти.
90+
91+
## 8. Заключение
92+
Алгоритм четно-нечетной перестановки успешно реализован с использованием MPI. Гибридный подход (локальная быстрая сортировка + глобальный обмен) показал высокую эффективность, продемонстрировав значительное ускорение по сравнению с наивной последовательной сортировкой пузырьком. Реализация устойчива к различным входным данным и корректно управляет ресурсами.
93+
94+
## 9. Список литературы
95+
1. лекции и практики курса "Параллельное программирование для кластерных систем";
96+
2. стандарт MPI (форум MPI);
97+
3. документация по C++;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#pragma once
2+
3+
#include "task/include/task.hpp"
4+
#include "volkov_a_odd_even_transposition/common/include/common.hpp"
5+
6+
namespace volkov_a_odd_even_transposition {
7+
8+
class OddEvenSortSeq : public BaseTask {
9+
public:
10+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
11+
return ppc::task::TypeOfTask::kSEQ;
12+
}
13+
14+
explicit OddEvenSortSeq(const InType &in);
15+
16+
private:
17+
bool ValidationImpl() override;
18+
bool PreProcessingImpl() override;
19+
bool RunImpl() override;
20+
bool PostProcessingImpl() override;
21+
};
22+
23+
} // namespace volkov_a_odd_even_transposition
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#include "volkov_a_odd_even_transposition/seq/include/ops_seq.hpp"
2+
3+
#include <algorithm>
4+
#include <cstddef>
5+
#include <utility>
6+
#include <vector>
7+
8+
#include "volkov_a_odd_even_transposition/common/include/common.hpp"
9+
10+
namespace volkov_a_odd_even_transposition {
11+
12+
OddEvenSortSeq::OddEvenSortSeq(const InType &in) {
13+
SetTypeOfTask(GetStaticTypeOfTask());
14+
GetInput() = in;
15+
}
16+
17+
bool OddEvenSortSeq::ValidationImpl() {
18+
return GetOutput().empty();
19+
}
20+
21+
bool OddEvenSortSeq::PreProcessingImpl() {
22+
GetOutput() = GetInput();
23+
return true;
24+
}
25+
26+
bool OddEvenSortSeq::RunImpl() {
27+
auto &arr = GetOutput();
28+
size_t n = arr.size();
29+
30+
if (n < 2) {
31+
return true;
32+
}
33+
34+
bool is_sorted = false;
35+
36+
while (!is_sorted) {
37+
is_sorted = true;
38+
39+
// Нечетная фаза
40+
for (size_t i = 1; i < n - 1; i += 2) {
41+
if (arr[i] > arr[i + 1]) {
42+
std::swap(arr[i], arr[i + 1]);
43+
is_sorted = false;
44+
}
45+
}
46+
47+
// Четная фаза
48+
for (size_t i = 0; i < n - 1; i += 2) {
49+
if (arr[i] > arr[i + 1]) {
50+
std::swap(arr[i], arr[i + 1]);
51+
is_sorted = false;
52+
}
53+
}
54+
}
55+
56+
return true;
57+
}
58+
59+
bool OddEvenSortSeq::PostProcessingImpl() {
60+
return true;
61+
}
62+
63+
} // namespace volkov_a_odd_even_transposition
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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
InheritParentConfig: true
2+
3+
Checks: >
4+
-modernize-loop-convert,
5+
-cppcoreguidelines-avoid-goto,
6+
-cppcoreguidelines-avoid-non-const-global-variables,
7+
-misc-use-anonymous-namespace,
8+
-modernize-use-std-print,
9+
-modernize-type-traits
10+
11+
CheckOptions:
12+
- key: readability-function-cognitive-complexity.Threshold
13+
value: 50 # Relaxed for tests

0 commit comments

Comments
 (0)