diff --git a/tasks/sokolov_k_shell_simple_merge/common/include/common.hpp b/tasks/sokolov_k_shell_simple_merge/common/include/common.hpp new file mode 100644 index 00000000..02169b86 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/common/include/common.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include "task/include/task.hpp" + +namespace sokolov_k_shell_simple_merge { + +using InType = int; +using OutType = int; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace sokolov_k_shell_simple_merge diff --git a/tasks/sokolov_k_shell_simple_merge/info.json b/tasks/sokolov_k_shell_simple_merge/info.json new file mode 100644 index 00000000..b06ba85a --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Кирилл", + "group_number": "3283Б1ПР4", + "last_name": "Соколов", + "middle_name": "Денисович", + "task_number": "3" + } +} diff --git a/tasks/sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp b/tasks/sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp new file mode 100644 index 00000000..4f8b2416 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#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 data_; +}; + +} // namespace sokolov_k_shell_simple_merge diff --git a/tasks/sokolov_k_shell_simple_merge/mpi/src/ops_mpi.cpp b/tasks/sokolov_k_shell_simple_merge/mpi/src/ops_mpi.cpp new file mode 100644 index 00000000..65ff2e20 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/mpi/src/ops_mpi.cpp @@ -0,0 +1,153 @@ +#include "sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp" + +#include + +#include +#include + +#include "sokolov_k_shell_simple_merge/common/include/common.hpp" + +namespace sokolov_k_shell_simple_merge { + +namespace { + +void ShellSort(std::vector &arr) { + int n = static_cast(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 SimpleMerge(const std::vector &a, const std::vector &b) { + std::vector result; + result.reserve(a.size() + b.size()); + int i = 0; + int j = 0; + int na = static_cast(a.size()); + int nb = static_cast(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 send_counts(size); + std::vector 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 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 merged; + for (int i = 0; i < size; i++) { + if (send_counts[i] > 0) { + std::vector 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 diff --git a/tasks/sokolov_k_shell_simple_merge/report.md b/tasks/sokolov_k_shell_simple_merge/report.md new file mode 100644 index 00000000..e9736f1d --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/report.md @@ -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: +4. Антонов А. С. Параллельное программирование с использованием технологии MPI. М.: Изд-во МГУ, 2004. +5. Воеводин В. В., Воеводин Вл. В. Параллельные вычисления. СПб.: БХВ-Петербург, 2002. + +## Приложение + +Фрагмент последовательной сортировки Шелла (seq/src/ops_seq.cpp, RunImpl): + +```cpp +int n = static_cast(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 SimpleMerge(const std::vector &a, const std::vector &b) { + std::vector result; + result.reserve(a.size() + b.size()); + int i = 0, j = 0; + int na = static_cast(a.size()); + int nb = static_cast(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; +} +``` diff --git a/tasks/sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp b/tasks/sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp new file mode 100644 index 00000000..17e22935 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "sokolov_k_shell_simple_merge/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace sokolov_k_shell_simple_merge { + +class SokolovKShellSimpleMergeSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit SokolovKShellSimpleMergeSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + std::vector data_; +}; + +} // namespace sokolov_k_shell_simple_merge diff --git a/tasks/sokolov_k_shell_simple_merge/seq/src/ops_seq.cpp b/tasks/sokolov_k_shell_simple_merge/seq/src/ops_seq.cpp new file mode 100644 index 00000000..282afc55 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/seq/src/ops_seq.cpp @@ -0,0 +1,60 @@ +#include "sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp" + +#include +#include + +#include "sokolov_k_shell_simple_merge/common/include/common.hpp" + +namespace sokolov_k_shell_simple_merge { + +SokolovKShellSimpleMergeSEQ::SokolovKShellSimpleMergeSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool SokolovKShellSimpleMergeSEQ::ValidationImpl() { + return (GetInput() > 0) && (GetOutput() == 0); +} + +bool SokolovKShellSimpleMergeSEQ::PreProcessingImpl() { + int n = GetInput(); + data_.resize(n); + for (int i = 0; i < n; i++) { + data_[i] = n - i; + } + return true; +} + +bool SokolovKShellSimpleMergeSEQ::RunImpl() { + int n = static_cast(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; + } + return true; +} + +bool SokolovKShellSimpleMergeSEQ::PostProcessingImpl() { + for (size_t i = 1; i < data_.size(); i++) { + if (data_[i - 1] > data_[i]) { + return false; + } + } + GetOutput() = GetInput(); + return true; +} + +} // namespace sokolov_k_shell_simple_merge diff --git a/tasks/sokolov_k_shell_simple_merge/settings.json b/tasks/sokolov_k_shell_simple_merge/settings.json new file mode 100644 index 00000000..16f25e42 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/settings.json @@ -0,0 +1,7 @@ +{ + "tasks": { + "mpi": "enabled", + "seq": "enabled" + }, + "tasks_type": "processes" +} diff --git a/tasks/sokolov_k_shell_simple_merge/tests/.clang-tidy b/tasks/sokolov_k_shell_simple_merge/tests/.clang-tidy new file mode 100644 index 00000000..ef43b7aa --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/tests/.clang-tidy @@ -0,0 +1,13 @@ +InheritParentConfig: true + +Checks: > + -modernize-loop-convert, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-non-const-global-variables, + -misc-use-anonymous-namespace, + -modernize-use-std-print, + -modernize-type-traits + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 50 # Relaxed for tests diff --git a/tasks/sokolov_k_shell_simple_merge/tests/functional/main.cpp b/tasks/sokolov_k_shell_simple_merge/tests/functional/main.cpp new file mode 100644 index 00000000..ab4e8930 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/tests/functional/main.cpp @@ -0,0 +1,75 @@ +#include + +#include +#include +#include +#include + +#include "sokolov_k_shell_simple_merge/common/include/common.hpp" +#include "sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp" +#include "sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace sokolov_k_shell_simple_merge { + +class SokolovKShellSimpleMergeFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + return std::to_string(std::get<0>(test_param)) + "_" + std::get<1>(test_param); + } + + protected: + void SetUp() override { + TestType params = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + input_data_ = std::get<0>(params); + } + + bool CheckTestOutputData(OutType &output_data) final { + return (input_data_ == output_data); + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_ = 0; +}; + +namespace { + +TEST_P(SokolovKShellSimpleMergeFuncTests, MatmulFromPic) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = {std::make_tuple(3, "3"), std::make_tuple(5, "5"), std::make_tuple(7, "7")}; + +const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_shell_simple_merge), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_sokolov_k_shell_simple_merge)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = SokolovKShellSimpleMergeFuncTests::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(PicMatrixTests, SokolovKShellSimpleMergeFuncTests, kGtestValues, kPerfTestName); + +const std::array kEdgeCaseParams = { + std::make_tuple(1, "single_element"), std::make_tuple(2, "two_elements"), + std::make_tuple(4, "four_elements"), std::make_tuple(10, "ten_elements"), + std::make_tuple(25, "twentyfive"), std::make_tuple(50, "fifty_elements"), + std::make_tuple(100, "hundred_elements"), std::make_tuple(500, "five_hundred")}; + +const auto kEdgeTestTasksList = std::tuple_cat(ppc::util::AddFuncTask( + kEdgeCaseParams, PPC_SETTINGS_sokolov_k_shell_simple_merge), + ppc::util::AddFuncTask( + kEdgeCaseParams, PPC_SETTINGS_sokolov_k_shell_simple_merge)); + +const auto kEdgeGtestValues = ppc::util::ExpandToValues(kEdgeTestTasksList); + +INSTANTIATE_TEST_SUITE_P(EdgeCaseTests, SokolovKShellSimpleMergeFuncTests, kEdgeGtestValues, kPerfTestName); + +} // namespace + +} // namespace sokolov_k_shell_simple_merge diff --git a/tasks/sokolov_k_shell_simple_merge/tests/performance/main.cpp b/tasks/sokolov_k_shell_simple_merge/tests/performance/main.cpp new file mode 100644 index 00000000..6f590ab9 --- /dev/null +++ b/tasks/sokolov_k_shell_simple_merge/tests/performance/main.cpp @@ -0,0 +1,41 @@ +#include + +#include "sokolov_k_shell_simple_merge/common/include/common.hpp" +#include "sokolov_k_shell_simple_merge/mpi/include/ops_mpi.hpp" +#include "sokolov_k_shell_simple_merge/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace sokolov_k_shell_simple_merge { + +class SokolovKShellSimpleMergePerfTests : public ppc::util::BaseRunPerfTests { + const int kCount_ = 500000; + InType input_data_{}; + + void SetUp() override { + input_data_ = kCount_; + } + + bool CheckTestOutputData(OutType &output_data) final { + return input_data_ == output_data; + } + + InType GetTestInputData() final { + return input_data_; + } +}; + +TEST_P(SokolovKShellSimpleMergePerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_sokolov_k_shell_simple_merge); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = SokolovKShellSimpleMergePerfTests::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, SokolovKShellSimpleMergePerfTests, kGtestValues, kPerfTestName); + +} // namespace sokolov_k_shell_simple_merge