Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tasks/sokolov_k_shell_simple_merge/common/include/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#pragma once

#include <string>
#include <tuple>

#include "task/include/task.hpp"

namespace sokolov_k_shell_simple_merge {

using InType = int;
using OutType = int;
using TestType = std::tuple<int, std::string>;
using BaseTask = ppc::task::Task<InType, OutType>;

} // namespace sokolov_k_shell_simple_merge
9 changes: 9 additions & 0 deletions tasks/sokolov_k_shell_simple_merge/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"student": {
"first_name": "Кирилл",
"group_number": "3283Б1ПР4",
"last_name": "Соколов",
"middle_name": "Денисович",
"task_number": "3"
}
}
26 changes: 26 additions & 0 deletions tasks/sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include <vector>

#include "sokolov_k_shell_simple_merge/common/include/common.hpp"
#include "task/include/task.hpp"

namespace sokolov_k_shell_simple_merge {

class SokolovKShellSimpleMergeMPI : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kMPI;
}
explicit SokolovKShellSimpleMergeMPI(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;

std::vector<int> data_;
};

} // namespace sokolov_k_shell_simple_merge
153 changes: 153 additions & 0 deletions tasks/sokolov_k_shell_simple_merge/mpi/src/ops_mpi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#include "sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp"

#include <mpi.h>

#include <cstddef>
#include <vector>

#include "sokolov_k_shell_simple_merge/common/include/common.hpp"

namespace sokolov_k_shell_simple_merge {

namespace {

void ShellSort(std::vector<int> &arr) {
int n = static_cast<int>(arr.size());
int gap = 1;
while (gap < n / 3) {
gap = (3 * gap) + 1;
}
while (gap >= 1) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
gap /= 3;
}
}

std::vector<int> SimpleMerge(const std::vector<int> &a, const std::vector<int> &b) {
std::vector<int> result;
result.reserve(a.size() + b.size());
int i = 0;
int j = 0;
int na = static_cast<int>(a.size());
int nb = static_cast<int>(b.size());
while (i < na && j < nb) {
if (a[i] <= b[j]) {
result.push_back(a[i]);
i++;
} else {
result.push_back(b[j]);
j++;
}
}
while (i < na) {
result.push_back(a[i]);
i++;
}
while (j < nb) {
result.push_back(b[j]);
j++;
}
return result;
}

} // namespace

SokolovKShellSimpleMergeMPI::SokolovKShellSimpleMergeMPI(const InType &in) {
SetTypeOfTask(GetStaticTypeOfTask());
GetInput() = in;
GetOutput() = 0;
}

bool SokolovKShellSimpleMergeMPI::ValidationImpl() {
return (GetInput() > 0) && (GetOutput() == 0);
}

bool SokolovKShellSimpleMergeMPI::PreProcessingImpl() {
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0) {
int n = GetInput();
data_.resize(n);
for (int i = 0; i < n; i++) {
data_[i] = n - i;
}
}
return true;
}

bool SokolovKShellSimpleMergeMPI::RunImpl() {
int rank = 0;
int size = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);

int n = GetInput();

std::vector<int> send_counts(size);
std::vector<int> displs(size);
int base_count = n / size;
int remainder = n % size;

for (int i = 0; i < size; i++) {
send_counts[i] = base_count + (i < remainder ? 1 : 0);
displs[i] = (i > 0) ? (displs[i - 1] + send_counts[i - 1]) : 0;
}

int local_size = send_counts[rank];
std::vector<int> local_data(local_size);

MPI_Scatterv(data_.data(), send_counts.data(), displs.data(), MPI_INT, local_data.data(), local_size, MPI_INT, 0,
MPI_COMM_WORLD);

ShellSort(local_data);

MPI_Gatherv(local_data.data(), local_size, MPI_INT, data_.data(), send_counts.data(), displs.data(), MPI_INT, 0,
MPI_COMM_WORLD);

if (rank == 0) {
std::vector<int> merged;
for (int i = 0; i < size; i++) {
if (send_counts[i] > 0) {
std::vector<int> chunk(data_.begin() + displs[i], data_.begin() + displs[i] + send_counts[i]);
merged = SimpleMerge(merged, chunk);
}
}
data_ = merged;
}

return true;
}

bool SokolovKShellSimpleMergeMPI::PostProcessingImpl() {
int rank = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);

int sorted = 1;
if (rank == 0) {
for (size_t i = 1; i < data_.size(); i++) {
if (data_[i - 1] > data_[i]) {
sorted = 0;
break;
}
}
}

MPI_Bcast(&sorted, 1, MPI_INT, 0, MPI_COMM_WORLD);

if (sorted == 0) {
return false;
}

GetOutput() = GetInput();
return true;
}

} // namespace sokolov_k_shell_simple_merge
180 changes: 180 additions & 0 deletions tasks/sokolov_k_shell_simple_merge/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Сортировка Шелла с простым слиянием

- Студент: Соколов Кирилл Денисович
- Группа: 3823Б1ПР4
- Технология: SEQ, MPI
- Вариант: 16

## 1. Введение

Лабораторная работа посвящена параллельной сортировке одномерного массива целых чисел. В качестве базового метода
выбрана сортировка Шелла. Для объединения отсортированных фрагментов после распределенной сортировки используется
простое слияние двух упорядоченных последовательностей. Цель работы: реализовать последовательный и параллельный
варианты алгоритма, провести замеры времени выполнения и оценить ускорение и эффективность при разном числе процессов.

## 2. Постановка задачи

**Вход:** целое число n (размер массива), n > 0. Массив из n элементов формируется на этапе
препроцессинга.

**Выход:** целое число (при успехе равно n. Используется для проверки корректности в тестах). Результат сортировки
хранится во внутреннем буфере задачи.

**Ограничения:** данные распределяются между процессами. В параллельном варианте требуется использование MPI_Send или
MPI_Scatterv в RunImpl. Горизонтальная схема: разбиение по "строкам" (блокам элементов вектора). При необходимости
вертикальная интерпретация сводится к разбиению того же вектора по столбцам/сегментам.

## 3. Базовый алгоритм

Используется сортировка Шелла с последовательностью шагов Кнута: 1, 4, 13, 40, ... (вид 3*gap+1, начиная с
gap=1).

1. Вычисление максимального шага: пока gap < n/3, полагаем gap = (3*gap)+1.
2. Пока gap >= 1:
- для каждого i от gap до n-1 выполняется вставка элемента data_[i] в упорядоченную подпоследовательность с шагом
gap (аналог сортировки вставками по индексам i, i-gap, i-2*gap, ...);
- затем gap = gap/3.
3. После последнего прохода (gap=1) массив отсортирован по неубыванию.

Сложность в среднем лучше квадратичной.

## 4. Схема распараллеливания (MPI)

- **Топология:** линейная; коммуникатор MPI_COMM_WORLD, ранги 0..P-1.
- **Распределение данных:** вектор длины n разбивается на P непрерывных блоков. Размеры блоков выравниваются:
первые n%P процессов получают по ceil(n/P) элементов, остальные по floor(n/P). Используется MPI_Scatterv для
рассылки с процесса 0.
- **Роли:**
- Ранг 0: формирует исходный массив в PreProcessingImpl, в RunImpl рассылает блоки (Scatterv), после Gatherv
выполняет простое слияние P отсортированных блоков в один массив, в PostProcessingImpl проверяет неубывание и
раздает результат проверки по процессам (MPI_Bcast).
- Ранги 1..P-1: получают свой блок (Scatterv), сортируют его локально сортировкой Шелла, отдают обратно (Gatherv),
участвуют в Bcast и выставляют выход.
- **Обмены:** Scatterv (распределение), Gatherv (сбор отсортированных блоков), Bcast (флаг корректности). В RunImpl
используется именно MPI_Scatterv, как требуется заданием.

Простое слияние: последовательное слияние двух упорядоченных массивов в один за один проход; на ранге 0 слияние
выполняется по цепочке (merge(merge(...(chunk_0, chunk_1), ...), chunk_{P-1})).

## 5. Детали реализации

- **Структура кода:** общие типы (InType, OutType, BaseTask) в common/include/common.hpp; последовательная
реализация в seq/include/ops_seq.hpp и seq/src/ops_seq.cpp; параллельная в mpi/include/ops_mpi.hpp и
mpi/src/ops_mpi.cpp. В MPI-версии локально определены функции ShellSort и SimpleMerge во внутреннем namespace.
- **Угловые случаи:** n < P обрабатывается корректно (часть процессов получает 0 элементов, пустые блоки не участвуют
в слиянии); пустой массив не допускается проверкой в ValidationImpl (n > 0).
- **Память:** на ранге 0 хранится полный массив data_; на каждом процессе - локальный блок и буферы для Scatterv/Gatherv;
при слиянии на ранге 0 используется временный вектор merged.

## 6. Экспериментальная установка

- **CPU:** Intel Core i5-10400kf
- **Ядра/Потоки:** 6/12
- **ОС:** Windows 10
- **Компилятор:** MSVC 14.44
- **Тип сборки:** Release
- **MPI реализация:** MS-MPI 10.0
- **CMake:** 4.2.0-rc1
- **Фреймворк тестирования:** Google Test
- **Данные:** массив размера n (в перф-тестах n задается константой, например 500000), заполнение обратным порядком
на ранге 0 в PreProcessingImpl

## 7. Результаты

### 7.1 Проверка корректности

Корректность проверяется функциональными тестами: для заданного n запускается полный пайплайн. В PostProcessingImpl
проверяется неубывание результирующего массива - при успехе выход задачи устанавливается в n. Тесты сравнивают выход с
ожидаемым значением для разных n (в том числе граничные случаи: 1, 2, малые и средние размеры). Для MPI результат
проверки (sorted) передается всем процессам через Bcast, чтобы все ранги вернули одинаковый результат.

### 7.2 Производительность

Базовое время для ускорения: MPI при 1 процессе (task_run 0.0359 с, pipeline 0.0499 с). Размер массива
n = 500000.

### task_run

| Процессов | Время, с | Ускорение | Эффективность |
|-----------|----------|-----------|---------------|
| 2 | 0.0285 | 1.26 | 62.9% |
| 4 | 0.0343 | 1.05 | 26.2% |
| 6 | 0.0441 | 0.81 | 13.6% |
| 8 | 0.0543 | 0.66 | 8.3% |

### task_pipeline

| Процессов | Время, с | Ускорение | Эффективность |
|-----------|----------|-----------|---------------|
| 2 | 0.0365 | 1.37 | 68.3% |
| 4 | 0.0393 | 1.27 | 31.8% |
| 6 | 0.0472 | 1.06 | 17.6% |
| 8 | 0.0575 | 0.87 | 10.9% |

Ускорение вычислено как T(1)/T(P), эффективность как (ускорение / P) * 100%. При росте числа процессов время
выполнения MPI-варианта увеличивается из-за накладных расходов (Scatterv/Gatherv, последовательное слияние на ранге
0), поэтому ускорение падает и при 6-8 процессах становится ниже 1.

## 8. Выводы

Реализованы последовательный и параллельный варианты сортировки Шелла с простым слиянием. В RunImpl используется
MPI_Scatterv для распределения данных и MPI_Gatherv для сбора отсортированных блоков. Финальное слияние выполняется на
ранге 0. Функциональные тесты подтверждают корректность для различных размеров и граничных случаев. Замеры
производительности показывают достижимое ускорение и эффективность при 2, 4, 6 и 8 процессах. Ограничения связаны с
коммуникациями и последовательным слиянием на одном процессе.

## 9. Источники

1. Кнут Д. Э. Искусство программирования. Т. 3. Сортировка и поиск. 2-е изд. М.: Вильямс, 2000.
2. Кормен Т., Лейзерсон Ч., Ривест Р., Штайн К. Алгоритмы: построение и анализ. 3-е изд. М.: Вильямс, 2013.
3. MPI Forum. MPI: A Message-Passing Interface Standard. Version 4.0. 2021. URL: <https://www.mpi-forum.org/docs/>
4. Антонов А. С. Параллельное программирование с использованием технологии MPI. М.: Изд-во МГУ, 2004.
5. Воеводин В. В., Воеводин Вл. В. Параллельные вычисления. СПб.: БХВ-Петербург, 2002.

## Приложение

Фрагмент последовательной сортировки Шелла (seq/src/ops_seq.cpp, RunImpl):

```cpp
int n = static_cast<int>(data_.size());
int gap = 1;
while (gap < n / 3) {
gap = (3 * gap) + 1;
}
while (gap >= 1) {
for (int i = gap; i < n; i++) {
int temp = data_[i];
int j = i;
while (j >= gap && data_[j - gap] > temp) {
data_[j] = data_[j - gap];
j -= gap;
}
data_[j] = temp;
}
gap /= 3;
}
```

Фрагмент простого слияния двух упорядоченных массивов (mpi/src/ops_mpi.cpp):

```cpp
std::vector<int> SimpleMerge(const std::vector<int> &a, const std::vector<int> &b) {
std::vector<int> result;
result.reserve(a.size() + b.size());
int i = 0, j = 0;
int na = static_cast<int>(a.size());
int nb = static_cast<int>(b.size());
while (i < na && j < nb) {
if (a[i] <= b[j]) {
result.push_back(a[i]);
i++;
} else {
result.push_back(b[j]);
j++;
}
}
while (i < na) { result.push_back(a[i]); i++; }
while (j < nb) { result.push_back(b[j]); j++; }
return result;
}
```
Loading
Loading