diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp b/tasks/yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp new file mode 100644 index 0000000000..ac84b7da68 --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "task/include/task.hpp" +#include "yushkova_p_hoare_sorting_simple_merging/common/include/common.hpp" + +namespace yushkova_p_hoare_sorting_simple_merging { + +class YushkovaPHoareSortingSimpleMergingALL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kALL; + } + explicit YushkovaPHoareSortingSimpleMergingALL(const InType &in); + + private: + static int HoarePartition(std::vector &values, int left, int right); + static void HoareQuickSort(std::vector &values, int left, int right); + static void SimpleMerge(const std::vector &source, std::vector &destination, std::size_t left, + std::size_t middle, std::size_t right); + static void SortLocalStlParallel(std::vector &values); + static void MergeGatheredChunks(std::vector &values, const std::vector &chunk_sizes, + const std::vector &offsets); + static void BroadcastVector(std::vector &values, int rank); + + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace yushkova_p_hoare_sorting_simple_merging diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/all/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/all/report.md new file mode 100644 index 0000000000..f4cf3ee3e8 --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/all/report.md @@ -0,0 +1,115 @@ +# Сортировка Хоара с простым слиянием - ALL + +- **Студент:** Юшкова Полина Александровна, 3823Б1ПР2 +- **Технология:** ALL (MPI + STL) +- **Вариант:** 13 + +## 1. Контекст + +ALL-версия объединяет MPI между процессами и `std::thread` внутри каждого процесса. MPI используется для распределения +частей массива между rank-ами, а локальная STL-схема сортирует фрагмент внутри процесса. + +## 2. Постановка задачи + +- **Входные данные:** непустой `std::vector`, доступный на корневом rank. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq(task_run) = 0.0030497600 s`, + `T_seq(pipeline) = 0.0083235000 s`. + +## 3. Базовый алгоритм + +Корневой rank хранит входной массив. Данные распределяются между rank-ами, каждый rank локально сортирует свой фрагмент +STL-схемой (блоки по 64 и простое слияние по уровням), затем фрагменты собираются на rank 0 и последовательно досливаются +в один общий массив. После этого итоговый вектор рассылается всем процессам. + +## 4. Межпроцессная схема + +`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов. `BuildDistribution` делит `total_size` почти +поровну: первые `remainder` rank-ов получают на один элемент больше. `MPI_Scatterv` отправляет локальные куски, +`MPI_Gatherv` собирает отсортированные куски на rank 0, затем `BroadcastVector` рассылает итог всем rank-ам через +`MPI_Bcast`. + +Фрагмент распределения и обмена: + +```cpp +std::vector chunk_sizes(static_cast(mpi_size)); +std::vector offsets(static_cast(mpi_size)); +BuildDistribution(total_size, mpi_size, chunk_sizes, offsets); + +const std::vector send_counts = MakeIntVector(chunk_sizes); +const std::vector send_offsets = MakeIntVector(offsets); + +std::vector local_data(chunk_sizes[static_cast(rank)]); +MPI_Scatterv(rank == 0 ? GetOutput().data() : nullptr, send_counts.data(), + send_offsets.data(), MPI_INT, local_data.data(), + send_counts[static_cast(rank)], MPI_INT, 0, + MPI_COMM_WORLD); + +SortLocalStlParallel(local_data); + +std::vector gathered_data; +if (rank == 0) { + gathered_data.resize(total_size); +} +MPI_Gatherv(local_data.data(), static_cast(local_data.size()), MPI_INT, + rank == 0 ? gathered_data.data() : nullptr, send_counts.data(), + send_offsets.data(), MPI_INT, 0, MPI_COMM_WORLD); +``` + +## 5. Внутрипроцессная схема + +Локальная сортировка выполняется функцией `SortLocalStlParallel`: + +- разбиение на блоки по 64 элемента; +- сортировка каждого блока quicksort Хоара; +- уровни простого слияния по независимым парам диапазонов. + +Задачи на каждом уровне выполняются через `RunInThreads` с распределением `task_index += thread_count`. Число потоков +выбирается по `std::thread::hardware_concurrency()` и числу задач (если `hardware_concurrency()==0`, используется +fallback `2`). + +В коде явного `MPI_Barrier` нет; синхронизация возникает на коллективных операциях `MPI_Scatterv`, `MPI_Gatherv` и +`MPI_Bcast`. + +## 6. Детали реализации + +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в выходной буфер. `RunImpl` выполняет +MPI-распределение, локальную сортировку, сбор, финальное последовательное слияние на rank 0 и broadcast результата. +`PostProcessingImpl` проверяет, что выход непустой и отсортирован. + +Файлы реализации: `all/include/ops_all.hpp`, `all/src/ops_all.cpp`. + +## 7. Проверка корректности + +ALL-backend зарегистрирован в общем функциональном тесте. Корректность подтверждается сравнением с эталоном +`std::ranges::sort` и дополнительными проверками `std::ranges::is_sorted`. + +## 8. Экспериментальная среда + +- **ОС:** Windows +- **MPI:** Microsoft MPI (`mpiexec`) +- **Compiler:** clang++ (MSVC toolchain), C++23 +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Baseline TaskRun:** `0.0030497600 s` +- **Baseline pipeline:** `0.0083235000 s` +- **Число повторов:** 5 + +## 9. Результаты + +- backend: all; ranks: 1; threads_per_rank: 12; total_workers: 12; time: 0.0159865600 s; speedup: 0.191; + efficiency: 0.016; notes: `mpiexec -n 1`, local STL auto-threads, `TaskRun`. +- backend: all; ranks: 2; threads_per_rank: 12; total_workers: 24; time: 0.0155627800 s; speedup: 0.196; + efficiency: 0.008; notes: `mpiexec -n 2`, local STL auto-threads, `TaskRun`. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: 0.0125462200 s; speedup: 0.243; + efficiency: 0.005; notes: `mpiexec -n 4`, local STL auto-threads, `TaskRun`. +- backend: all; ranks: 1; threads_per_rank: 12; total_workers: 12; time: 0.0173728400 s; speedup: 0.479; + efficiency: 0.040; notes: `mpiexec -n 1`, local STL auto-threads, `pipeline`. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: 0.0142562000 s; speedup: 0.584; + efficiency: 0.012; notes: `mpiexec -n 4`, local STL auto-threads, `pipeline`. + +## 10. Выводы + +ALL-версия добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на rank 0. На `N=100000` итоговое время +существенно зависит от коммуникаций и накладных расходов на создание локальных потоков, поэтому эффективность по +`total_workers` получается низкой, даже если локальная сортировка внутри ранга выполняется параллельно. diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/all/src/ops_all.cpp b/tasks/yushkova_p_hoare_sorting_simple_merging/all/src/ops_all.cpp new file mode 100644 index 0000000000..96df88d692 --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/all/src/ops_all.cpp @@ -0,0 +1,295 @@ +#include "yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "yushkova_p_hoare_sorting_simple_merging/common/include/common.hpp" + +namespace yushkova_p_hoare_sorting_simple_merging { + +namespace { + +constexpr std::size_t kBlockSize = 64; + +std::size_t GetThreadCount(std::size_t task_count) { + if (task_count == 0) { + return 0; + } + const unsigned int hardware_threads = std::thread::hardware_concurrency(); + const std::size_t available_threads = hardware_threads == 0 ? 2 : static_cast(hardware_threads); + return std::min(task_count, available_threads); +} + +template +void RunSequentialTasks(std::size_t task_count, Function function) { + for (std::size_t task_index = 0; task_index < task_count; ++task_index) { + function(task_index); + } +} + +template +void RunThreadWorker(std::size_t thread_index, std::size_t thread_count, std::size_t task_count, Function &function, + std::exception_ptr &exception_ptr, std::mutex &exception_mutex) { + try { + for (std::size_t task_index = thread_index; task_index < task_count; task_index += thread_count) { + function(task_index); + } + } catch (...) { + std::scoped_lock lock(exception_mutex); + if (!exception_ptr) { + exception_ptr = std::current_exception(); + } + } +} + +template +void RunInThreads(std::size_t task_count, Function function) { + const std::size_t thread_count = GetThreadCount(task_count); + if (thread_count <= 1) { + RunSequentialTasks(task_count, function); + return; + } + + std::vector threads; + threads.reserve(thread_count); + std::exception_ptr exception_ptr; + std::mutex exception_mutex; + for (std::size_t thread_index = 0; thread_index < thread_count; ++thread_index) { + threads.emplace_back([thread_index, thread_count, task_count, &function, &exception_ptr, &exception_mutex]() { + RunThreadWorker(thread_index, thread_count, task_count, function, exception_ptr, exception_mutex); + }); + } + + for (auto &thread : threads) { + thread.join(); + } + + if (exception_ptr) { + std::rethrow_exception(exception_ptr); + } +} + +std::vector MakeIntVector(const std::vector &values) { + std::vector result(values.size()); + for (std::size_t i = 0; i < values.size(); ++i) { + result[i] = static_cast(values[i]); + } + return result; +} + +void BuildDistribution(std::size_t total_size, int mpi_size, std::vector &chunk_sizes, + std::vector &offsets) { + const std::size_t base_size = total_size / static_cast(mpi_size); + const std::size_t remainder = total_size % static_cast(mpi_size); + for (int rank = 0; rank < mpi_size; ++rank) { + const auto index = static_cast(rank); + chunk_sizes[index] = base_size + (index < remainder ? 1U : 0U); + offsets[index] = rank == 0 ? 0 : offsets[index - 1] + chunk_sizes[index - 1]; + } +} + +} // namespace + +YushkovaPHoareSortingSimpleMergingALL::YushkovaPHoareSortingSimpleMergingALL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +int YushkovaPHoareSortingSimpleMergingALL::HoarePartition(std::vector &values, int left, int right) { + const int pivot = values[left + ((right - left) / 2)]; + int i = left - 1; + int j = right + 1; + + while (true) { + ++i; + while (values[i] < pivot) { + ++i; + } + + --j; + while (values[j] > pivot) { + --j; + } + + if (i >= j) { + return j; + } + + std::swap(values[i], values[j]); + } +} + +void YushkovaPHoareSortingSimpleMergingALL::HoareQuickSort(std::vector &values, int left, int right) { + std::stack> ranges; + ranges.emplace(left, right); + + while (!ranges.empty()) { + auto [current_left, current_right] = ranges.top(); + ranges.pop(); + + if (current_left >= current_right) { + continue; + } + + const int partition_index = HoarePartition(values, current_left, current_right); + + if ((partition_index - current_left) > (current_right - (partition_index + 1))) { + ranges.emplace(current_left, partition_index); + ranges.emplace(partition_index + 1, current_right); + } else { + ranges.emplace(partition_index + 1, current_right); + ranges.emplace(current_left, partition_index); + } + } +} + +void YushkovaPHoareSortingSimpleMergingALL::SimpleMerge(const std::vector &source, std::vector &destination, + std::size_t left, std::size_t middle, std::size_t right) { + std::size_t left_index = left; + std::size_t right_index = middle; + std::size_t destination_index = left; + + while (left_index < middle && right_index < right) { + if (source[left_index] <= source[right_index]) { + destination[destination_index++] = source[left_index++]; + } else { + destination[destination_index++] = source[right_index++]; + } + } + + while (left_index < middle) { + destination[destination_index++] = source[left_index++]; + } + + while (right_index < right) { + destination[destination_index++] = source[right_index++]; + } +} + +void YushkovaPHoareSortingSimpleMergingALL::SortLocalStlParallel(std::vector &values) { + if (values.size() <= 1) { + return; + } + + const std::size_t size = values.size(); + const std::size_t block_count = (size + kBlockSize - 1) / kBlockSize; + + RunInThreads(block_count, [&values, size](std::size_t block_index) { + const std::size_t block_start = block_index * kBlockSize; + const std::size_t block_end = std::min(block_start + kBlockSize, size); + if ((block_end - block_start) > 1) { + HoareQuickSort(values, static_cast(block_start), static_cast(block_end - 1)); + } + }); + + for (std::size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + std::vector merged_data(size); + const std::size_t merge_count = (size + (2 * merge_width) - 1) / (2 * merge_width); + + RunInThreads(merge_count, [&values, size, merge_width, &merged_data](std::size_t merge_index) { + const std::size_t left = merge_index * 2 * merge_width; + const std::size_t middle = std::min(left + merge_width, size); + const std::size_t right = std::min(left + (2 * merge_width), size); + + if (middle < right) { + SimpleMerge(values, merged_data, left, middle, right); + } else { + std::copy(values.begin() + static_cast(left), + values.begin() + static_cast(right), + merged_data.begin() + static_cast(left)); + } + }); + + values.swap(merged_data); + } +} + +void YushkovaPHoareSortingSimpleMergingALL::MergeGatheredChunks(std::vector &values, + const std::vector &chunk_sizes, + const std::vector &offsets) { + std::vector merged_data(values.size()); + for (std::size_t rank = 1; rank < chunk_sizes.size(); ++rank) { + const std::size_t left = 0; + const std::size_t middle = offsets[rank]; + const std::size_t right = middle + chunk_sizes[rank]; + SimpleMerge(values, merged_data, left, middle, right); + std::copy(merged_data.begin(), merged_data.begin() + static_cast(right), values.begin()); + } +} + +void YushkovaPHoareSortingSimpleMergingALL::BroadcastVector(std::vector &values, int rank) { + auto size = static_cast(values.size()); + MPI_Bcast(&size, 1, MPI_UINT64_T, 0, MPI_COMM_WORLD); + if (rank != 0) { + values.resize(static_cast(size)); + } + if (size > 0) { + MPI_Bcast(values.data(), static_cast(size), MPI_INT, 0, MPI_COMM_WORLD); + } +} + +bool YushkovaPHoareSortingSimpleMergingALL::ValidationImpl() { + return !GetInput().empty(); +} + +bool YushkovaPHoareSortingSimpleMergingALL::PreProcessingImpl() { + GetOutput() = GetInput(); + return true; +} + +bool YushkovaPHoareSortingSimpleMergingALL::RunImpl() { + int rank = 0; + int mpi_size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + + const std::size_t total_size = GetOutput().size(); + if (total_size <= 1) { + return true; + } + + std::vector chunk_sizes(static_cast(mpi_size)); + std::vector offsets(static_cast(mpi_size)); + BuildDistribution(total_size, mpi_size, chunk_sizes, offsets); + + const std::vector send_counts = MakeIntVector(chunk_sizes); + const std::vector send_offsets = MakeIntVector(offsets); + + std::vector local_data(chunk_sizes[static_cast(rank)]); + MPI_Scatterv(rank == 0 ? GetOutput().data() : nullptr, send_counts.data(), send_offsets.data(), MPI_INT, + local_data.data(), send_counts[static_cast(rank)], MPI_INT, 0, MPI_COMM_WORLD); + + SortLocalStlParallel(local_data); + + std::vector gathered_data; + if (rank == 0) { + gathered_data.resize(total_size); + } + MPI_Gatherv(local_data.data(), static_cast(local_data.size()), MPI_INT, + rank == 0 ? gathered_data.data() : nullptr, send_counts.data(), send_offsets.data(), MPI_INT, 0, + MPI_COMM_WORLD); + + if (rank == 0) { + MergeGatheredChunks(gathered_data, chunk_sizes, offsets); + GetOutput() = std::move(gathered_data); + } + + BroadcastVector(GetOutput(), rank); + return std::ranges::is_sorted(GetOutput()); +} + +bool YushkovaPHoareSortingSimpleMergingALL::PostProcessingImpl() { + return !GetOutput().empty() && std::ranges::is_sorted(GetOutput()); +} + +} // namespace yushkova_p_hoare_sorting_simple_merging diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/omp/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/omp/report.md new file mode 100644 index 0000000000..24d0d0022a --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/omp/report.md @@ -0,0 +1,154 @@ +# Сортировка Хоара с простым слиянием - OMP + +- Студент: Юшкова Полина Александровна +- Группа: 3823Б1ПР2 +- Технология: OMP +- Вариант: 13 + +## 1. Контекст + +OpenMP-версия расширяет базовую сортировку Хоара за счет параллельной обработки независимых блоков и последующего +простого слияния. Я делю входной массив на несколько частей, сортирую их одновременно, а затем объединяю уже +упорядоченные части в один итоговый массив. + +## 2. Постановка задачи + +В этой работе я реализовала параллельную сортировку массива целых чисел с использованием OpenMP. В моей реализации: + +- корректно обрабатывать входные данные; +- разбивать массив на независимые участки; +- выполнять локальную сортировку каждого участка алгоритмом Хоара; +- выполнять последующее слияние отсортированных частей; +- возвращать отсортированный массив и подтверждать корректность результата. + +## 3. Базовый алгоритм + +В основе решения лежат две классические операции: + +1. **Разбиение Хоара**. Опорный элемент выбирается из середины текущего диапазона. Два указателя двигаются навстречу + друг другу, после чего элементы, нарушающие порядок относительно опорного значения, меняются местами. +2. **Простое слияние**. После локальной сортировки блоков два отсортированных диапазона объединяются в один + результирующий массив последовательным сравнением текущих элементов. + +Для сортировки используется не рекурсивная версия, а итеративная реализация с собственным стеком диапазонов. +Это упрощает контроль над глубиной вызовов и делает алгоритм удобнее для применения внутри блоков. + +## 4. Схема распараллеливания + +Параллелизм применяется только на этапе локальной сортировки блоков: + +- входной массив делится на `chunks`; +- число блоков ограничивается количеством доступных потоков и размером массива; +- каждый блок сортируется независимо в директиве `#pragma omp parallel for`; +- после завершения параллельной фазы выполняется последовательное слияние блоков слева направо. + +Такой подход позволяет избежать гонок данных на этапе сортировки, потому что каждый поток работает только со своим +диапазоном индексов. + +## 5. Детали реализации + +Реализация находится в файлах: + +- `omp/include/ops_omp.hpp` +- `omp/src/ops_omp.cpp` + +Ключевые этапы работы: + +- в конструкторе устанавливается тип задачи и копируется входной массив; +- `ValidationImpl()` принимает только непустой вход; +- `PreProcessingImpl()` переносит данные из входа в выход; +- `RunImpl()` запускает сортировку и слияние; +- `PostProcessingImpl()` дополнительно проверяет, что результат не пустой и отсортирован. + +### 5.1 Сортировка блока + +Функция `HoareQuickSort()` использует стек пар границ. Для каждого диапазона вызывается `HoarePartition()`. +После этого поддиапазоны добавляются обратно в стек, что позволяет обойтись без рекурсии. + +### 5.2 Слияние + +Функция `Merge()` объединяет два уже отсортированных участка массива: + +- левый участок: `[left, mid]`; +- правый участок: `[mid + 1, right]`. + +Временный буфер `merged` заполняется по мере выбора минимального элемента из двух текущих позиций. Затем данные +записываются обратно в исходный массив. + +### 5.3 Управление числом блоков + +Число блоков определяется как: + +- `max_threads = max(1, omp_get_max_threads())`; +- `chunks = min(max_threads, n)`. + +Если массив слишком мал, сортировка выполняется без распараллеливания. Так я избегаю лишних накладных расходов. + +### 5.4 Основной фрагмент + +```cpp +const int max_threads = std::max(1, omp_get_max_threads()); +const int chunks = std::min(max_threads, n); + +std::vector borders(static_cast(chunks + 1)); +for (int i = 0; i <= chunks; ++i) { + borders[static_cast(i)] = (i * n) / chunks; +} + +#pragma omp parallel for default(none) shared(values, borders, chunks) +for (int chunk = 0; chunk < chunks; ++chunk) { + const int left = borders[static_cast(chunk)]; + const int right = borders[static_cast(chunk) + 1] - 1; + if (left < right) { + HoareQuickSort(values, left, right); + } +} +``` + +## 6. Проверка корректности + +Корректность проверяется на нескольких уровнях: + +- `ValidationImpl()` не пропускает пустой массив; +- функциональные тесты сравнивают результат с эталонной сортировкой; +- `PostProcessingImpl()` проверяет, что выходной массив отсортирован; +- тесты покрывают маленькие входы; +- отдельно проверяются массивы с повторяющимися значениями, отрицательными числами и уже отсортированными данными. + +## 7. Экспериментальная среда + +- ОС: Windows +- Компилятор: C++17 +- Технология параллелизма: OpenMP +- Набор тестов: Google Test +- Среда измерений: `ppc_perf_tests` + +## 8. Результаты + +Измерения выполнены на массиве из `100000` целых чисел. + +- mode: seq; threads: 1; time: 0.003516 s; speedup: 1.000; efficiency: 100.0%. +- mode: omp; threads: 2; time: 0.001898 s; speedup: 1.853; efficiency: 92.6%. +- mode: omp; threads: 4; time: 0.001549 s; speedup: 2.269; efficiency: 56.7%. +- mode: omp; threads: 8; time: 0.001683 s; speedup: 2.089; efficiency: 26.1%. + +Анализ результатов следует делать по двум критериям: + +- наличие ускорения относительно последовательной версии; +- изменение эффективности при росте числа потоков. + +В моей реализации выигрыш от OpenMP заметен уже на размере `100000`. При этом рост числа потоков после 4 начинает +снижать эффективность, потому что часть работы остается последовательной. Также влияют накладные расходы на создание +и синхронизацию потоков. + +## 9. Выводы + +В ходе работы я реализовала OpenMP-версию сортировки Хоара с простым слиянием. Алгоритм корректно: + +- разделяет входной массив на независимые блоки; +- сортирует каждый блок отдельно; +- объединяет отсортированные части в итоговый массив; +- проходит функциональные тесты на корректность. + +Моя реализация является простой и предсказуемой по структуре. Параллелизм применяется только там, где нет конфликтов +по данным, поэтому решение удобно сравнивать с последовательной и другими параллельными версиями. diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/report.md new file mode 100644 index 0000000000..7ffe6a103b --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/report.md @@ -0,0 +1,117 @@ +# Сортировка Хоара с простым слиянием - сводный отчет + +- **Студент:** Юшкова Полина Александровна, 3823Б1ПР2 +- **Технологии:** SEQ, OMP, TBB, STL, ALL (MPI + STL) +- **Вариант:** 13 + +## 1. Контекст + +Работа сравнивает реализации сортировки `std::vector` по неубыванию для backend-ов `seq`, `omp`, `tbb`, `stl` и +`all`. Последовательная версия задает baseline, потоковые версии проверяют разные модели внутрипроцессного +распараллеливания, а ALL добавляет MPI-уровень. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** объект `std::vector` того же размера, элементы которого переставлены в порядке неубывания. +- **Эталон корректности:** совпадение с результатом `std::ranges::sort` в функциональных тестах. +- **Baseline:** `seq`, `T_seq(task_run) = 0.0030497600 s`, `T_seq(pipeline) = 0.0083235000 s`. + +Единые типы входа и выхода заданы в `common/include/common.hpp`. + +## 3. Базовый алгоритм + +Базовое вычислительное ядро использует сортировку Хоара: опорный элемент берется из середины диапазона, затем диапазон +разбивается двумя индексами и сортируется итеративным quicksort. + +Параллельные реализации используют принцип простого слияния: + +- локальная сортировка независимых частей (половин или блоков по 64 элемента); +- уровни слияния объединяют соседние отсортированные диапазоны. + +## 4. Схемы распараллеливания + +- **SEQ:** две половины, сортировка Хоара, `SimpleMerge`. +- **OMP:** деление массива на `chunks`, сортировка блоков в `#pragma omp parallel for`, затем последовательное слияние. +- **TBB:** `oneapi::tbb::parallel_for(blocked_range)` для сортировки блоков по 64 и для каждого прохода слияния. +- **STL:** при `PPC_NUM_THREADS > 1` используется 2 worker-а (один `std::thread` + текущий поток) для сортировки двух + половин, затем merge. +- **ALL:** MPI распределяет фрагменты между rank-ами (`Scatterv/Gatherv/Bcast`), а локальная часть каждого rank-а + сортирует фрагмент STL-схемой (блоки по 64 и уровни слияния). + +## 5. Детали методики + +Замеры выполнены на `N=100000` случайных `int` из диапазона `[-1000000, 1000000]`, число повторов - 5. + +- `TaskRun`: повторяется только `Run()` после одного `PreProcessing()`. +- `pipeline`: каждый повтор проходит полный pipeline (`Validation` → `PreProcessing` → `Run` → `PostProcessing`). + +Speedup считался как `T_seq / T_backend`, efficiency - как `speedup / workers`. + +## 6. Проверка корректности + +Функциональные тесты регистрируют все backend-ы (`seq/omp/stl/tbb/all`) и сравнивают результат с `std::ranges::sort`. +Дополнительно в реализациях используются проверки `std::ranges::is_sorted`. + +## 7. Экспериментальная среда + +- **ОС:** Windows +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 +- **ALL:** `mpiexec` (Microsoft MPI) +- **Auto workers (ALL локально):** `std::thread::hardware_concurrency() = 12` на тестовой машине + +## 8. Результаты TaskRun + +- backend: seq; time: 0.0030497600 s; speedup: 1.000; efficiency: 1.000; notes: baseline. +- backend: omp, 1 thread; time: 0.0029892200 s; speedup: 1.020; efficiency: 1.020; notes: `PPC_NUM_THREADS=1`. +- backend: omp, 2 threads; time: 0.0021860800 s; speedup: 1.395; efficiency: 0.698; notes: `PPC_NUM_THREADS=2`. +- backend: omp, 4 threads; time: 0.0018365600 s; speedup: 1.661; efficiency: 0.415; notes: `PPC_NUM_THREADS=4`. +- backend: stl, workers: 1; time: 0.0028924000 s; speedup: 1.054; efficiency: 1.054; notes: `PPC_NUM_THREADS=1`. +- backend: stl, workers: 2; time: 0.0021834000 s; speedup: 1.397; efficiency: 0.698; + notes: `PPC_NUM_THREADS=4` включает параллельную ветку. +- backend: tbb, 1 worker; time: 0.0076817000 s; speedup: 0.397; efficiency: 0.397; + notes: `PPC_NUM_THREADS=1`, `global_control`. +- backend: tbb, 2 workers; time: 0.0048485400 s; speedup: 0.629; efficiency: 0.315; + notes: `PPC_NUM_THREADS=2`, `global_control`. +- backend: tbb, 4 workers; time: 0.0028551400 s; speedup: 1.068; efficiency: 0.267; + notes: `PPC_NUM_THREADS=4`, `global_control`. +- backend: all; ranks: 1; threads_per_rank: 12; total_workers: 12; time: 0.0159865600 s; + speedup: 0.191; efficiency: 0.016; notes: `mpiexec -n 1`. +- backend: all; ranks: 2; threads_per_rank: 12; total_workers: 24; time: 0.0155627800 s; + speedup: 0.196; efficiency: 0.008; notes: `mpiexec -n 2`. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: 0.0125462200 s; + speedup: 0.243; efficiency: 0.005; notes: `mpiexec -n 4`. + +## 9. Результаты Pipeline + +- backend: seq; time: 0.0083235000 s; speedup: 1.000; efficiency: 1.000; notes: baseline. +- backend: omp, 1 thread; time: 0.0112251400 s; speedup: 0.742; efficiency: 0.742; notes: `PPC_NUM_THREADS=1`. +- backend: omp, 4 threads; time: 0.0037493600 s; speedup: 2.220; efficiency: 0.555; notes: `PPC_NUM_THREADS=4`. +- backend: stl, workers: 1; time: 0.0086819200 s; speedup: 0.959; efficiency: 0.959; notes: `PPC_NUM_THREADS=1`. +- backend: stl, workers: 2; time: 0.0055119600 s; speedup: 1.510; efficiency: 0.755; + notes: `PPC_NUM_THREADS=4`. +- backend: tbb, 1 worker; time: 0.0113887600 s; speedup: 0.731; efficiency: 0.731; + notes: `PPC_NUM_THREADS=1`, `global_control`. +- backend: tbb, 4 workers; time: 0.0037098400 s; speedup: 2.244; efficiency: 0.561; + notes: `PPC_NUM_THREADS=4`, `global_control`. +- backend: all; ranks: 1; threads_per_rank: 12; total_workers: 12; time: 0.0173728400 s; + speedup: 0.479; efficiency: 0.040; notes: `mpiexec -n 1`. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: 0.0142562000 s; + speedup: 0.584; efficiency: 0.012; notes: `mpiexec -n 4`. + +## 10. Интерпретация результатов + +`seq` служит baseline. `omp` показывает ускорение на `TaskRun` и заметный выигрыш на pipeline при 4 потоках, так как часть +работы распараллеливается по независимым блокам. `stl` ускоряет сортировку половин только при включении параллельной +ветки (2 worker-а). `tbb` на `N=100000` чувствителен к накладным расходам `parallel_for` и выделения буферов слияния: +в `TaskRun` speedup заметен только при 4 потоках, а в pipeline - уверенный выигрыш при 4 потоках. +`all` добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на rank 0, из-за чего эффективность по +`total_workers` остается низкой. + +## 11. Выводы + +Для измеренного размера `N=100000` лучший pipeline-результат среди потоковых backend-ов показали `tbb(4)` и `omp(4)` +(времена порядка `0.0037 s`). ALL-версия на `4` rank-ах не ускоряет baseline на этом размере из-за MPI-обменов и +накладных расходов локальной сортировки. diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/seq/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/seq/report.md new file mode 100644 index 0000000000..fa44d9d63d --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/seq/report.md @@ -0,0 +1,100 @@ +# Сортировка Хоара с простым слиянием - SEQ + +- **Студент:** Юшкова Полина Александровна, 3823Б1ПР2 +- **Технология:** SEQ +- **Вариант:** 13 + +## 1. Контекст + +Последовательная версия является базовым эталоном для всех остальных реализаций задачи. Она сортирует `std::vector` +по неубыванию и не использует параллельных примитивов, поэтому ее время берется как `T_seq` при расчете ускорения и +эффективности параллельных версий. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** объект `std::vector` того же размера, элементы которого переставлены в порядке неубывания. +- **Эталон корректности:** результат `std::ranges::sort` в функциональных тестах. + +Единые типы входа и выхода заданы в `common/include/common.hpp`. + +## 3. Базовый алгоритм + +Вычислительное ядро использует разбиение Хоара и итеративный quicksort. Опорный элемент выбирается из середины диапазона, +два индекса двигаются навстречу друг другу, а элементы меняются местами до пересечения индексов. + +В моей реализации дополнительно используется простое слияние: массив делится на две половины, каждая половина +сортируется алгоритмом Хоара, затем половины объединяются процедурой merge. + +Средняя сложность сортировки составляет `O(n log n)`, худшая - `O(n^2)`. Дополнительная память тратится на стек +диапазонов и временные буферы при слиянии, суммарно до `O(n)`. + +Фрагмент реализации разбиения: + +```cpp +int YushkovaPHoareSortingSimpleMergingSEQ::HoarePartition( + std::vector &values, int left, int right) { + int pivot = values[left + ((right - left) / 2)]; + int i = left - 1; + int j = right + 1; + while (true) { + ++i; + while (values[i] < pivot) { + ++i; + } + --j; + while (values[j] > pivot) { + --j; + } + if (i >= j) { + return j; + } + std::swap(values[i], values[j]); + } +} +``` + +## 4. Детали реализации + +`ValidationImpl` принимает только непустой вход. `PreProcessingImpl` копирует вход в выходной буфер, чтобы сортировать +результат без изменения исходного контейнера. + +`RunImpl` пропускает массивы размера `0/1`, затем: + +1. Делит вход на `left` и `right`. +2. Вызывает `HoareQuickSort` для каждой части (если размер больше 1). +3. Объединяет результат функцией `SimpleMerge`. +4. Проверяет `std::ranges::is_sorted`. + +`PostProcessingImpl` повторно проверяет непустой и отсортированный выход. Это фиксирует базовый pipeline задачи: +валидация, подготовка, сортировка и проверка результата. + +Файлы реализации: `seq/include/ops_seq.hpp`, `seq/src/ops_seq.cpp`. + +## 5. Проверка корректности + +Функциональные тесты строят эталон через `std::ranges::sort(expected)` и сравнивают его с выходом задачи. Набор содержит +10 сценариев (один элемент, два элемента, уже отсортированный, обратный порядок, дубликаты, смешанные знаки и т.д.). + +## 6. Экспериментальная среда + +- **ОС:** Windows +- **Compiler:** MinGW g++ (C++23) +- **Флаги:** `-O2` +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 + +Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного `PreProcessing()`. + +## 7. Результаты + +Baseline измерен локальным замером на `N=100000` (5 повторов). + +- backend: seq; time: 0.0030497600 s; speedup: 1.000; efficiency: 1.000; notes: `N=100000`, `task_run`. +- backend: seq; time: 0.0083235000 s; speedup: 1.000; efficiency: 1.000; notes: `N=100000`, `pipeline`. + +## 8. Выводы + +Последовательная версия подтверждает корректность алгоритма сравнением с `std::ranges::sort` и задает baseline для всех +параллельных backend-ов. Для `N=100000` время `task_run` составило `0.0030497600 s`, а pipeline-замер - `0.0083235000 s`. diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/stl/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/stl/report.md new file mode 100644 index 0000000000..a1d6dd1191 --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/stl/report.md @@ -0,0 +1,93 @@ +# Сортировка Хоара с простым слиянием - STL + +- **Студент:** Юшкова Полина Александровна, 3823Б1ПР2 +- **Технология:** STL +- **Вариант:** 13 + +## 1. Контекст + +STL-версия использует `std::thread` для параллельной сортировки частей массива и последующего простого слияния. +Реализация ориентирована на минимальные накладные расходы: при `PPC_NUM_THREADS == 1` сортировка выполняется +последовательно, а при `PPC_NUM_THREADS > 1` создается один дополнительный поток для сортировки одной из половин массива. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq(task_run) = 0.0030497600 s`, + `T_seq(pipeline) = 0.0083235000 s`. + +## 3. Базовый алгоритм + +Массив делится на две половины. Каждая половина сортируется итеративным quicksort Хоара, после чего результат +объединяется через простое слияние (merge). Для merge используется `std::ranges::merge` в выходной буфер. + +## 4. Схема распараллеливания + +Параллельная ветка включается, если `ppc::util::GetNumThreads()` (читается из `PPC_NUM_THREADS`) больше 1. +Фактически при этом создаются **2 worker-а**: один поток сортирует левую половину, а текущий поток сортирует правую. + +Для корректного проброса исключений из дочернего потока используется `std::exception_ptr`. + +Фрагмент распараллеливания: + +```cpp +std::exception_ptr exception_ptr; +std::thread left_worker([&] { + try { + SortHalfIfNeeded(left); + } catch (...) { + exception_ptr = std::current_exception(); + } +}); + +try { + SortHalfIfNeeded(right); +} catch (...) { + if (!exception_ptr) { + exception_ptr = std::current_exception(); + } +} + +left_worker.join(); +if (exception_ptr) { + std::rethrow_exception(exception_ptr); +} +``` + +## 5. Детали реализации + +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в выходной буфер. +`RunImpl` делит массив пополам, сортирует половины (последовательно или параллельно) и выполняет `SimpleMerge`. +`PostProcessingImpl` дополнительно проверяет, что выход непустой и отсортирован. + +Файлы реализации: `stl/include/ops_stl.hpp`, `stl/src/ops_stl.cpp`. + +## 6. Проверка корректности + +STL-backend подключен в общий список задач функционального теста, а эталон строится через `std::ranges::sort`. +Дополнительно в `RunImpl` и `PostProcessingImpl` используется `std::ranges::is_sorted`. + +## 7. Экспериментальная среда + +- **ОС:** Windows +- **Compiler:** MinGW g++ (C++23) +- **Флаги:** `-O2` +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 + +## 8. Результаты + +- workers: 1; time: 0.0028924000 s; speedup: 1.054; efficiency: 1.054; notes: `TaskRun`; `PPC_NUM_THREADS=1`. +- workers: 1; time: 0.0086819200 s; speedup: 0.959; efficiency: 0.959; notes: `pipeline`; `PPC_NUM_THREADS=1`. +- workers: 2; time: 0.0021834000 s; speedup: 1.397; efficiency: 0.698; notes: `TaskRun`; + `PPC_NUM_THREADS=4` включает параллельную ветку. +- workers: 2; time: 0.0055119600 s; speedup: 1.510; efficiency: 0.755; notes: `pipeline`; + `PPC_NUM_THREADS=4` включает параллельную ветку. + +## 9. Выводы + +STL-версия повторяет базовые фазы решения: сортировка двух независимых половин и простое слияние. +При `PPC_NUM_THREADS > 1` используется 2 потока, что дает ускорение относительно SEQ на `N=100000`: +для `task_run` время составило `0.0021834000 s` (speedup `1.397`), для pipeline - `0.0055119600 s` (speedup `1.510`). diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/tbb/report.md b/tasks/yushkova_p_hoare_sorting_simple_merging/tbb/report.md new file mode 100644 index 0000000000..99f2d9e443 --- /dev/null +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/tbb/report.md @@ -0,0 +1,94 @@ +# Сортировка Хоара с простым слиянием - TBB + +- **Студент:** Юшкова Полина Александровна, 3823Б1ПР2 +- **Технология:** TBB +- **Вариант:** 13 + +## 1. Контекст + +TBB-версия переносит две независимые фазы алгоритма на `oneapi::tbb`: сортировку блоков и слияние соседних +отсортированных диапазонов. В этой реализации я проверяю, насколько эффективно планировщик TBB распределяет работу на +входе `N=100000`. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq(task_run) = 0.0030497600 s`, + `T_seq(pipeline) = 0.0083235000 s`. + +## 3. Базовый алгоритм + +Массив `int` сортируется блоками по 64 элемента, затем соседние отсортированные диапазоны объединяются простым слиянием. +Локальная сортировка использует схему Хоара: pivot из середины, два индекса и обмен до пересечения. + +## 4. Схема распараллеливания + +Используется `oneapi::tbb::parallel_for` с `oneapi::tbb::blocked_range`: + +- для сортировки блоков по индексам `block_index`; +- для каждого уровня слияния - по индексам `merge_index` независимых пар диапазонов. + +Grainsize и partitioner явно не задаются, используются значения по умолчанию TBB. + +Фрагмент TBB-части: + +```cpp +const size_t block_count = (size + kBlockSize - 1U) / kBlockSize; +oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0U, block_count), + [&data, size](const oneapi::tbb::blocked_range &range) { + for (size_t block_index = range.begin(); block_index != range.end(); ++block_index) { + SortBlockIfNeeded(data, size, block_index); + } +}); + +for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + MergePass(data, size, merge_width); +} +``` + +## 5. Детали реализации + +`ValidationImpl`, `PreProcessingImpl`, `RunImpl` и `PostProcessingImpl` расположены в `tbb/src/ops_tbb.cpp`. + +- Сортировка блоков пишет в непересекающиеся отрезки `data_`. +- На каждом проходе слияния создается буфер `merged_data(size)` и параллельно заполняются непересекающиеся диапазоны: + чтение из `data_`, запись в `merged_data`. +- После завершения `parallel_for` выполняется `data_.swap(merged_data)`. + +Важно: `PreProcessingImpl` копирует вход в `data_`, а `GetOutput()` заполняется только если `data_` оказался +отсортированным (`std::ranges::is_sorted(data_)`). + +Файлы реализации: `tbb/include/ops_tbb.hpp`, `tbb/src/ops_tbb.cpp`. + +## 6. Проверка корректности + +TBB-backend подключен в общий список задач функционального теста, а эталон строится через `std::ranges::sort`. +Внутри реализации дополнительно используется проверка `std::ranges::is_sorted`. + +## 7. Экспериментальная среда + +- **ОС:** Windows +- **Compiler:** C++ +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Baseline TaskRun:** `0.0030497600 s` +- **Baseline pipeline:** `0.0083235000 s` +- **Число повторов:** 5 по умолчанию + +## 8. Результаты + +Замеры выполнены на `N=100000` (5 повторов) с ограничением конкуренции через +`oneapi::tbb::global_control(max_allowed_parallelism, PPC_NUM_THREADS)`. + +- threads: 1; time: 0.0076817000 s; speedup: 0.397; efficiency: 0.397; notes: `TaskRun`, `PPC_NUM_THREADS=1`. +- threads: 2; time: 0.0048485400 s; speedup: 0.629; efficiency: 0.315; notes: `TaskRun`, `PPC_NUM_THREADS=2`. +- threads: 4; time: 0.0028551400 s; speedup: 1.068; efficiency: 0.267; notes: `TaskRun`, `PPC_NUM_THREADS=4`. +- threads: 1; time: 0.0113887600 s; speedup: 0.731; efficiency: 0.731; notes: `pipeline`, `PPC_NUM_THREADS=1`. +- threads: 4; time: 0.0037098400 s; speedup: 2.244; efficiency: 0.561; notes: `pipeline`, `PPC_NUM_THREADS=4`. + +## 9. Выводы + +TBB-версия реализует независимую сортировку блоков и независимое слияние на каждом уровне, поэтому масштабируется лучше +простых вариантов, если объем работы достаточно большой. Для `N=100000` итог зависит от накладных расходов `parallel_for` +и выделения временных буферов на проходах слияния; для других размеров нужны отдельные замеры. diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/tests/functional/main.cpp b/tasks/yushkova_p_hoare_sorting_simple_merging/tests/functional/main.cpp index f55570dcb6..31c0ca10a5 100644 --- a/tasks/yushkova_p_hoare_sorting_simple_merging/tests/functional/main.cpp +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/tests/functional/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -10,6 +11,7 @@ #include "util/include/func_test_util.hpp" #include "util/include/util.hpp" +#include "yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp" #include "yushkova_p_hoare_sorting_simple_merging/common/include/common.hpp" #include "yushkova_p_hoare_sorting_simple_merging/omp/include/ops_omp.hpp" #include "yushkova_p_hoare_sorting_simple_merging/seq/include/ops_seq.hpp" @@ -31,6 +33,14 @@ class YushkovaPRunFuncTestsThreads : public ppc::util::BaseRunFuncTests( kTestParam, PPC_SETTINGS_yushkova_p_hoare_sorting_simple_merging), + ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_yushkova_p_hoare_sorting_simple_merging), ppc::util::AddFuncTask( kTestParam, PPC_SETTINGS_yushkova_p_hoare_sorting_simple_merging)); diff --git a/tasks/yushkova_p_hoare_sorting_simple_merging/tests/performance/main.cpp b/tasks/yushkova_p_hoare_sorting_simple_merging/tests/performance/main.cpp index 588a79dc7f..594ac0f639 100644 --- a/tasks/yushkova_p_hoare_sorting_simple_merging/tests/performance/main.cpp +++ b/tasks/yushkova_p_hoare_sorting_simple_merging/tests/performance/main.cpp @@ -1,10 +1,13 @@ #include +#include #include #include #include #include "util/include/perf_test_util.hpp" +#include "util/include/util.hpp" +#include "yushkova_p_hoare_sorting_simple_merging/all/include/ops_all.hpp" #include "yushkova_p_hoare_sorting_simple_merging/common/include/common.hpp" #include "yushkova_p_hoare_sorting_simple_merging/omp/include/ops_omp.hpp" #include "yushkova_p_hoare_sorting_simple_merging/seq/include/ops_seq.hpp" @@ -30,6 +33,14 @@ class YushkovaPRunPerfTestsThreads : public ppc::util::BaseRunPerfTests b; }) == output_data.end(); @@ -48,7 +59,8 @@ namespace { const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( + YushkovaPHoareSortingSimpleMergingSTL, YushkovaPHoareSortingSimpleMergingALL, + YushkovaPHoareSortingSimpleMergingTBB>( PPC_SETTINGS_yushkova_p_hoare_sorting_simple_merging); const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks);