diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp new file mode 100644 index 0000000000..4d9d2f4738 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace olesnitskiy_v_hoare_sort_simple_merge { + +class OlesnitskiyVHoareSortSimpleMergeALL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kALL; + } + explicit OlesnitskiyVHoareSortSimpleMergeALL(const InType &in); + + private: + static int HoarePartition(std::vector &array, int left, int right); + static void HoareQuickSort(std::vector &array, int left, int right); + static void SimpleMerge(const std::vector &source, std::vector &destination, size_t left, size_t middle, + size_t right); + static void SortLocalStlParallel(std::vector &array); + static void MergeGatheredChunks(std::vector &array, const std::vector &chunk_sizes, + const std::vector &offsets); + + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + std::vector data_; +}; + +} // namespace olesnitskiy_v_hoare_sort_simple_merge diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md new file mode 100644 index 0000000000..95171937f9 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md @@ -0,0 +1,112 @@ +# Сортировка Хоара с простым слиянием — 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 = 0.0058254364 s`. + +## 3. Базовый алгоритм + +Корневой rank хранит входной массив. Данные распределяются между rank-ами, +каждый rank локально сортирует свой фрагмент STL-схемой, затем фрагменты +собираются на 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); + +std::vector send_counts = MakeIntVector(chunk_sizes); +std::vector send_offsets = MakeIntVector(offsets); +std::vector local_data(chunk_sizes[static_cast(mpi_rank)]); + +MPI_Scatterv(data_.data(), send_counts.data(), send_offsets.data(), MPI_INT, + local_data.data(), send_counts[mpi_rank], MPI_INT, 0, + MPI_COMM_WORLD); + +SortLocalStlParallel(local_data); + +std::vector gathered_data; +if (mpi_rank == 0) { + gathered_data.resize(total_size); +} +``` + +## 5. Внутрипроцессная схема + +Локальная сортировка повторяет STL-версию: `RunInThreads`, блоки по 64, затем +уровни слияния. Количество потоков внутри rank-а выбирается по +`std::thread::hardware_concurrency()` и числу локальных задач. + +Для ALL важно указывать `ranks`, `threads_per_rank` и +`total_workers = ranks * threads_per_rank`. В коде явного `MPI_Barrier` нет; +обязательная синхронизация возникает на коллективных операциях `MPI_Scatterv`, +`MPI_Gatherv` и `MPI_Bcast`. Дополнительный `MPI_Barrier` добавляет тестовый +listener раннера после каждого теста. + +## 6. Детали реализации + +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в +`data_`. `RunImpl` выполняет MPI-распределение, локальную сортировку, сбор и +broadcast. `PostProcessingImpl` проверяет `std::ranges::is_sorted` и записывает +выход. + +## 7. Проверка корректности + +ALL-backend зарегистрирован в общем функциональном тесте. Запуск под +`mpirun -np 2` с фильтром +`*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*` прошел 15/15 ALL-тестов. +Остальные backend-ы (`seq/omp/stl/tbb`) прошли 60 тестов вне `mpirun`. + +## 8. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Baseline TaskRun:** `0.0058254364 s` +- **Baseline pipeline:** `0.0068995056 s` +- **Число повторов:** 5 по умолчанию + +## 9. Результаты + +- backend: all; ranks: 1; threads_per_rank: 12; total_workers: 12; time: + 0.0126647332 s; speedup: 0.460; efficiency: 0.038; notes: `mpirun -np 1`, + `PPC_NUM_THREADS=1`, local STL auto-threads. +- backend: all; ranks: 2; threads_per_rank: 12; total_workers: 24; time: + 0.0088037862 s; speedup: 0.662; efficiency: 0.028; notes: `mpirun -np 2`, + local STL auto-threads. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: + 0.0043261284 s; speedup: 1.347; efficiency: 0.028; notes: `mpirun -np 4`, + local STL auto-threads. +- backend: all; ranks: 4; threads_per_rank: 12; total_workers: 48; time: + 0.0516886980 s; speedup: 0.133; efficiency: 0.003; notes: `pipeline`, + `mpirun -np 4`, local STL auto-threads. + +## 10. Выводы + +ALL-версия добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на +rank 0. На 4 rank-ах получено `0.0043261284 s`, speedup `1.347`; efficiency +`0.028`, потому что каждый rank дополнительно запускает до 12 STL-потоков, а +коммуникации остаются обязательными. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/src/ops_all.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/src/ops_all.cpp new file mode 100644 index 0000000000..283f709b83 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/src/ops_all.cpp @@ -0,0 +1,276 @@ +#include "olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" + +namespace olesnitskiy_v_hoare_sort_simple_merge { + +namespace { + +constexpr size_t kBlockSize = 64; + +size_t GetThreadCount(size_t task_count) { + if (task_count == 0) { + return 0; + } + + const unsigned int hardware_threads = std::thread::hardware_concurrency(); + const size_t available_threads = hardware_threads == 0 ? 2 : static_cast(hardware_threads); + return std::min(task_count, available_threads); +} + +template +void RunInThreads(size_t task_count, Function function) { + const size_t thread_count = GetThreadCount(task_count); + if (thread_count <= 1) { + for (size_t task_index = 0; task_index < task_count; ++task_index) { + function(task_index); + } + return; + } + + std::vector threads; + threads.reserve(thread_count); + + for (size_t thread_index = 0; thread_index < thread_count; ++thread_index) { + threads.emplace_back([thread_index, thread_count, task_count, &function]() { + for (size_t task_index = thread_index; task_index < task_count; task_index += thread_count) { + function(task_index); + } + }); + } + + for (auto &thread : threads) { + thread.join(); + } +} + +std::vector MakeIntVector(const std::vector &values) { + std::vector result(values.size()); + for (size_t i = 0; i < values.size(); ++i) { + result[i] = static_cast(values[i]); + } + return result; +} + +void BuildDistribution(size_t total_size, int mpi_size, std::vector &chunk_sizes, + std::vector &offsets) { + const size_t base_size = total_size / static_cast(mpi_size); + const 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]; + } +} + +void BroadcastVector(std::vector &data, int mpi_rank) { + size_t size = data.size(); + MPI_Bcast(&size, 1, MPI_UNSIGNED_LONG, 0, MPI_COMM_WORLD); + + if (mpi_rank != 0) { + data.resize(size); + } + if (size > 0) { + MPI_Bcast(data.data(), static_cast(size), MPI_INT, 0, MPI_COMM_WORLD); + } +} + +} // namespace + +OlesnitskiyVHoareSortSimpleMergeALL::OlesnitskiyVHoareSortSimpleMergeALL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +int OlesnitskiyVHoareSortSimpleMergeALL::HoarePartition(std::vector &array, int left, int right) { + const int pivot = array[left + ((right - left) / 2)]; + int i = left - 1; + int j = right + 1; + + while (true) { + ++i; + while (array[i] < pivot) { + ++i; + } + + --j; + while (array[j] > pivot) { + --j; + } + + if (i >= j) { + return j; + } + + std::swap(array[i], array[j]); + } +} + +void OlesnitskiyVHoareSortSimpleMergeALL::HoareQuickSort(std::vector &array, int left, int right) { + std::stack> stack; + stack.emplace(left, right); + + while (!stack.empty()) { + auto [current_left, current_right] = stack.top(); + stack.pop(); + + if (current_left >= current_right) { + continue; + } + + const int middle = HoarePartition(array, current_left, current_right); + + if ((middle - current_left) > (current_right - (middle + 1))) { + stack.emplace(current_left, middle); + stack.emplace(middle + 1, current_right); + } else { + stack.emplace(middle + 1, current_right); + stack.emplace(current_left, middle); + } + } +} + +void OlesnitskiyVHoareSortSimpleMergeALL::SimpleMerge(const std::vector &source, std::vector &destination, + size_t left, size_t middle, size_t right) { + size_t left_index = left; + size_t right_index = middle; + 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 OlesnitskiyVHoareSortSimpleMergeALL::SortLocalStlParallel(std::vector &array) { + if (array.size() <= 1) { + return; + } + + const size_t size = array.size(); + const size_t block_count = (size + kBlockSize - 1) / kBlockSize; + + RunInThreads(block_count, [&array, size](size_t block_index) { + const size_t block_start = block_index * kBlockSize; + const size_t block_end = std::min(block_start + kBlockSize, size); + if ((block_end - block_start) > 1) { + HoareQuickSort(array, static_cast(block_start), static_cast(block_end - 1)); + } + }); + + for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + std::vector merged_data(size); + const size_t merge_count = (size + (2 * merge_width) - 1) / (2 * merge_width); + + RunInThreads(merge_count, [&array, size, merge_width, &merged_data](size_t merge_index) { + const size_t left = merge_index * 2 * merge_width; + const size_t middle = std::min(left + merge_width, size); + const size_t right = std::min(left + (2 * merge_width), size); + + if (middle < right) { + SimpleMerge(array, merged_data, left, middle, right); + } else { + std::copy(array.begin() + static_cast(left), array.begin() + static_cast(right), + merged_data.begin() + static_cast(left)); + } + }); + + array.swap(merged_data); + } +} + +void OlesnitskiyVHoareSortSimpleMergeALL::MergeGatheredChunks(std::vector &array, + const std::vector &chunk_sizes, + const std::vector &offsets) { + std::vector merged_data(array.size()); + for (size_t rank = 1; rank < chunk_sizes.size(); ++rank) { + const size_t left = 0; + const size_t middle = offsets[rank]; + const size_t right = middle + chunk_sizes[rank]; + SimpleMerge(array, merged_data, left, middle, right); + std::copy(merged_data.begin(), merged_data.begin() + static_cast(right), array.begin()); + } +} + +bool OlesnitskiyVHoareSortSimpleMergeALL::ValidationImpl() { + return !GetInput().empty(); +} + +bool OlesnitskiyVHoareSortSimpleMergeALL::PreProcessingImpl() { + data_ = GetInput(); + GetOutput().clear(); + return true; +} + +bool OlesnitskiyVHoareSortSimpleMergeALL::RunImpl() { + int mpi_rank = 0; + int mpi_size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_rank); + MPI_Comm_size(MPI_COMM_WORLD, &mpi_size); + + const size_t total_size = data_.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); + + std::vector send_counts = MakeIntVector(chunk_sizes); + std::vector send_offsets = MakeIntVector(offsets); + std::vector local_data(chunk_sizes[static_cast(mpi_rank)]); + + MPI_Scatterv(data_.data(), send_counts.data(), send_offsets.data(), MPI_INT, local_data.data(), send_counts[mpi_rank], + MPI_INT, 0, MPI_COMM_WORLD); + + SortLocalStlParallel(local_data); + + std::vector gathered_data; + if (mpi_rank == 0) { + gathered_data.resize(total_size); + } + + MPI_Gatherv(local_data.data(), static_cast(local_data.size()), MPI_INT, gathered_data.data(), send_counts.data(), + send_offsets.data(), MPI_INT, 0, MPI_COMM_WORLD); + + if (mpi_rank == 0) { + MergeGatheredChunks(gathered_data, chunk_sizes, offsets); + data_ = std::move(gathered_data); + } + + BroadcastVector(data_, mpi_rank); + return std::ranges::is_sorted(data_); +} + +bool OlesnitskiyVHoareSortSimpleMergeALL::PostProcessingImpl() { + const bool is_sorted = std::ranges::is_sorted(data_); + if (is_sorted) { + GetOutput() = data_; + } + return is_sorted; +} + +} // namespace olesnitskiy_v_hoare_sort_simple_merge diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md new file mode 100644 index 0000000000..aabc642b0f --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md @@ -0,0 +1,114 @@ +# Сортировка Хоара с простым слиянием — OMP + +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** OMP +- **Вариант:** 13 + +## 1. Контекст + +OpenMP-версия расширяет базовую сортировку Хоара за счет параллельной обработки +независимых блоков и последующего простого слияния. Цель реализации — проверить, +окупаются ли параллельные циклы OpenMP на входе `N=100000`. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq = 0.0058254364 s`. + +## 3. Базовый алгоритм + +Последовательное ядро использует разбиение Хоара и quicksort по локальным +диапазонам. После локальной сортировки блоков по 64 элемента выполняется +попарное простое слияние до полного массива. + +Средняя сложность quicksort-части составляет `O(n log n)`. Слияние добавляет +линейный проход на каждом уровне объединения блоков. + +## 4. Схема распараллеливания + +OpenMP используется в двух `parallel for`: + +1. сортировка независимых блоков; +2. слияние независимых пар блоков. + +В директивах используется `default(none)`, поэтому общие переменные перечислены +явно: `size`, `data`, `merged_data`, `merge_width`. Индексы циклов и временные +векторы являются локальными для тела цикла. Reduction не требуется, так как +общей скалярной агрегации нет. + +Фрагмент параллельной части: + +```cpp +#pragma omp parallel for default(none) shared(size, data) + for (std::size_t block_start = 0; block_start < size; + block_start += kBlockSize) { + const std::size_t block_end = std::min(block_start + kBlockSize, size); + if ((block_end - block_start) > 1) { + HoareQuickSort(data, 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); + +#pragma omp parallel for default(none) \ + shared(merge_width, size, merged_data, data) + for (std::size_t left = 0; left < size; left += (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); +``` + +## 5. Детали реализации + +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в +`data_` и очищает выход. `RunImpl` сортирует блоки и выполняет итеративное +слияние. `PostProcessingImpl` проверяет `data_` через +`std::ranges::is_sorted` и только после этого записывает `GetOutput()`. + +При сортировке блок `block_start..block_end` не пересекается с соседним, потому +что шаг равен `kBlockSize`. При слиянии каждый поток пишет в свой диапазон +`merged_data[left, right)`. Общий `data` на фазе слияния только читается, а +`data.swap(merged_data)` выполняется после завершения параллельного цикла. + +## 6. Проверка корректности + +Эталон в функциональных тестах строится через `std::ranges::sort`. Запуск +`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests` с фильтром +`*olesnitskiy_v_hoare_sort_simple_merge*` прошел: 75 tests, 60 passed, 15 +ALL-tests skipped вне `mpirun`. ALL отдельно прошел под `mpirun -np 2`: 15 +passed. + +## 7. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Baseline TaskRun:** `0.0058254364 s` +- **Baseline pipeline:** `0.0068995056 s` +- **Число повторов:** 5 по умолчанию + +Потоки задавались через `PPC_NUM_THREADS`. + +## 8. Результаты + +- threads: 1; time: 0.2042550788 s; speedup: 0.029; efficiency: 0.029; notes: + `TaskRun`, `N=100000`. +- threads: 2; time: 0.2022266690 s; speedup: 0.029; efficiency: 0.014; notes: + `TaskRun`, `N=100000`. +- threads: 4; time: 0.1880041056 s; speedup: 0.031; efficiency: 0.008; notes: + `TaskRun`, `N=100000`. +- threads: 1; time: 0.1145523454 s; speedup: 0.060; efficiency: 0.060; notes: + `pipeline`, `N=100000`. +- threads: 4; time: 0.1193738506 s; speedup: 0.058; efficiency: 0.014; notes: + `pipeline`, `N=100000`. + +## 9. Выводы + +На данном размере входа OpenMP-версия медленнее baseline: лучший `TaskRun` +замер `0.1880041056 s` при 4 потоках дает speedup `0.031`. Основная причина — +много мелких блоков `kBlockSize=64` и дополнительные временные векторы при +слиянии. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md new file mode 100644 index 0000000000..06e491bf5f --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -0,0 +1,175 @@ +# Сортировка Хоара с простым слиянием — сводный отчет + +- **Студент:** Олесницкий Владимир Тарасович, 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`, `PPC_NUM_THREADS=1`, `T_seq = 0.0058254364 s`. + +Единые типы входа и выхода заданы в `common/include/common.hpp`. + +## 3. Базовый алгоритм + +Последовательное ядро использует сортировку Хоара. Массив разбивается по +опорному элементу из середины диапазона, затем поддиапазоны сортируются +итеративным quicksort. + +Параллельные реализации используют общий принцип простого слияния: сначала +сортируются независимые блоки по 64 элемента, затем уровни слияния объединяют +соседние отсортированные диапазоны. + +## 4. Схемы распараллеливания + +- **OMP:** два `parallel for` для сортировки блоков и слияния пар диапазонов. +- **TBB:** `oneapi::tbb::parallel_for` с `blocked_range`. +- **STL:** ручной запуск `std::thread`; число потоков берется из + `std::thread::hardware_concurrency()`. +- **ALL:** MPI распределяет фрагменты между rank-ами, а локальная часть каждого + rank-а использует STL-сортировку. + +Для OMP/TBB применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно +ограничивается `tbb::global_control`. STL и локальная часть ALL берут число +потоков из `std::thread::hardware_concurrency()`. На тестовой машине `nproc = +12`, поэтому такие строки обозначены как auto workers. + +## 5. Детали методики + +Performance-тест генерирует `N=100000` случайных `int` из диапазона +`[-1000000, 1000000]`. Использован `TaskRun`; режим выбирается в раннере. +Speedup считался как `T_seq / T_backend`, efficiency — как +`speedup / workers`. + +Ограничение методики: `TaskRun` повторяет только `Run()` после одного +`PreProcessing()`, а вход создается заново для каждого gtest-параметра через +`std::random_device`. Поэтому дополнительно сняты pipeline-замеры, где каждый +повтор проходит полный pipeline. + +## 6. Проверка корректности + +Функциональные тесты регистрируют все пять backend-ов. Запуск +`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests` с фильтром +`*olesnitskiy_v_hoare_sort_simple_merge*` выполнил 75 тестов: 60 passed для +`seq/omp/stl/tbb`, 15 ALL skipped вне MPI. + +Отдельный запуск `mpirun -np 2 ./build_olesnitskiy/bin/ppc_func_tests` с +фильтром `*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*` прошел 15/15 +ALL-тестов. + +## 7. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 по умолчанию +- **Auto workers:** 12 на тестовой машине + +## 8. Результаты TaskRun + +- backend: seq; time: 0.0058254364 s; speedup: 1.000; efficiency: 1.000; notes: + `TaskRun`, `N=100000`, baseline. +- backend: omp, 1 thread; time: 0.2042550788 s; speedup: 0.029; efficiency: + 0.029; notes: много мелких блоков `kBlockSize=64`. +- backend: omp, 2 threads; time: 0.2022266690 s; speedup: 0.029; efficiency: + 0.014; notes: `PPC_NUM_THREADS=2`. +- backend: omp, 4 threads; time: 0.1880041056 s; speedup: 0.031; efficiency: + 0.008; notes: лучший OMP-замер. +- backend: tbb, 1 worker; time: 0.0024417256 s; speedup: 2.386; efficiency: + 2.386; notes: `blocked_range`, `parallel_for`. +- backend: tbb, 2 workers; time: 0.0014976288 s; speedup: 3.889; efficiency: + 1.945; notes: `PPC_NUM_THREADS=2`. +- backend: tbb, 4 workers; time: 0.0011774682 s; speedup: 4.947; efficiency: + 1.237; notes: лучший потоковый backend. +- backend: stl, auto workers (12 на тестовой машине); time: 0.0047718214 s; + speedup: 1.221; efficiency: 0.102; notes: `hardware_concurrency`; без env. +- backend: all, 1 rank x 12 threads; time: 0.0126647332 s; speedup: 0.460; + efficiency: 0.038; notes: total_workers=12. +- backend: all, 2 ranks x 12 threads; time: 0.0088037862 s; speedup: 0.662; + efficiency: 0.028; notes: total_workers=24. +- backend: all, 4 ranks x 12 threads; time: 0.0043261284 s; speedup: 1.347; + efficiency: 0.028; notes: total_workers=48. + +## 9. Результаты PipelineRun + +- backend: seq; time: 0.0068995056 s; speedup: 1.000; efficiency: 1.000; notes: + `pipeline`, baseline. +- backend: omp, 1 thread; time: 0.1145523454 s; speedup: 0.060; efficiency: + 0.060; notes: `pipeline`. +- backend: omp, 4 threads; time: 0.1193738506 s; speedup: 0.058; efficiency: + 0.014; notes: `pipeline`. +- backend: stl, auto workers (12 на тестовой машине); time: 0.0055675704 s; + speedup: 1.239; efficiency: 0.103; notes: `pipeline`; auto. +- backend: tbb, 1 worker; time: 0.0067520042 s; speedup: 1.022; efficiency: + 1.022; notes: `pipeline`. +- backend: tbb, 4 workers; time: 0.0026312252 s; speedup: 2.622; efficiency: + 0.656; notes: `pipeline`. +- backend: all, 4 ranks x 12 threads; time: 0.0516886980 s; speedup: 0.133; + efficiency: 0.003; notes: `pipeline`, total_workers=48. + +## 10. Интерпретация результатов + +`seq` сортирует весь массив одним quicksort и служит baseline. `omp` создает +параллельные области для множества блоков по 64 и выделяет временные векторы при +слиянии; на `N=100000` измеренный speedup меньше 1. + +`tbb` использует `parallel_for(blocked_range)` для блоков и слияний; при 4 +workers получено `4.947x`. `stl` вручную создает и join-ит потоки; при 12 +workers получено `1.221x`, но efficiency равна `0.102`. + +`all` добавляет `MPI_Scatterv/Gatherv/Bcast` и финальное слияние на rank 0. На +4 rank-ах время `0.0043261284 s`, но efficiency равна `0.028`. + +## 11. Репродуцируемость + +Сборка: + +```bash +cmake -S . -B build_olesnitskiy -DCMAKE_BUILD_TYPE=Release +cmake --build build_olesnitskiy --target ppc_func_tests ppc_perf_tests -j 4 +``` + +Запуск корректности: + +```bash +PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests \ + --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge*' +OMPI_ALLOW_RUN_AS_ROOT=1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 \ + mpirun -np 2 ./build_olesnitskiy/bin/ppc_func_tests \ + --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*' +``` + +Запуск замеров: + +```bash +PPC_NUM_THREADS=1 ./build_olesnitskiy/bin/ppc_perf_tests \ + --gtest_filter='*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled' +PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_perf_tests \ + --gtest_filter='*task_run*olesnitskiy_v_hoare_sort_simple_merge_tbb_enabled' +OMPI_ALLOW_RUN_AS_ROOT=1 OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 PPC_NUM_THREADS=1 \ + mpirun -np 4 ./build_olesnitskiy/bin/ppc_perf_tests \ + --gtest_filter='*task_run*olesnitskiy_v_hoare_sort_simple_merge_all_enabled' +PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_perf_tests \ + --gtest_filter='*pipeline*olesnitskiy_v_hoare_sort_simple_merge_tbb_enabled' +``` + +## 12. Выводы + +Для измеренного размера `N=100000` лучшая численная версия — TBB с 4 workers: +`0.0011774682 s`, speedup `4.947`. ALL на 4 rank-ах быстрее baseline +(`0.0043261284 s`, speedup `1.347`), но его efficiency низкая из-за 48 +суммарных workers и MPI-обменов. OMP на этом входе проигрывает baseline из-за +накладных расходов. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md new file mode 100644 index 0000000000..e3d7c05a66 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md @@ -0,0 +1,116 @@ +# Сортировка Хоара с простым слиянием — 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. Опорный +элемент выбирается из середины диапазона, два индекса двигаются навстречу друг +другу, а элементы меняются местами до пересечения индексов. + +Средняя сложность сортировки составляет `O(n log n)`, худшая — `O(n^2)`. +Дополнительная память тратится на стек диапазонов и в неблагоприятном случае +может достигать `O(n)`. + +Фрагмент реализации разбиения: + +```cpp +int OlesnitskiyVHoareSortSimpleMergeSEQ::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]); + } +} +``` + +## 4. Детали реализации + +`ValidationImpl` принимает только непустой вход. `PreProcessingImpl` копирует +вход в выходной буфер, чтобы сортировать результат без изменения исходного +контейнера. `RunImpl` пропускает массивы размера `0/1`, затем вызывает +`HoareQuickSort` и проверяет `std::ranges::is_sorted`. + +`PostProcessingImpl` повторно проверяет непустой и отсортированный выход. Это +фиксирует базовый pipeline задачи: валидация, подготовка, сортировка и проверка +результата. + +## 5. Проверка корректности + +Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и +сравнивает его с выходом задачи. Набор содержит 15 сценариев: один элемент, +дубликаты, обратный порядок, отрицательные значения, границу блока 64 и +`int`-пределы. + +Запуск `PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests` с фильтром +`*olesnitskiy_v_hoare_sort_simple_merge*` прошел: 75 тестов, 60 passed и 15 +ALL-тестов skipped вне `mpirun`. Отдельный запуск `mpirun -np 2 +./build_olesnitskiy/bin/ppc_func_tests` с фильтром +`*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*` прошел 15/15 ALL-тестов. + +## 6. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 по умолчанию в `Perf::TaskRun` + +Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного +`PreProcessing()`, а вход для каждого gtest-параметра генерируется заново через +`std::random_device`. + +## 7. Результаты + +Baseline измерен командой: `PPC_NUM_THREADS=1 +./build_olesnitskiy/bin/ppc_perf_tests` с фильтром +`*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled`. + +- backend: seq; time: 0.0058254364 s; speedup: 1.000; efficiency: 1.000; notes: + `N=100000`, `TaskRun`, `PPC_NUM_THREADS=1`. +- backend: seq; time: 0.0068995056 s; speedup: 1.000; efficiency: 1.000; notes: + `N=100000`, `pipeline`, `PPC_NUM_THREADS=1`. + +## 8. Выводы + +Последовательная версия подтверждает корректность алгоритма сравнением с +`std::ranges::sort` и задает baseline для всех параллельных backend-ов. Для +`N=100000` время `TaskRun` составило `0.0058254364 s`, а pipeline-замер — +`0.0068995056 s`. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp new file mode 100644 index 0000000000..68ddfabb2e --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace olesnitskiy_v_hoare_sort_simple_merge { + +class OlesnitskiyVHoareSortSimpleMergeSTL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSTL; + } + explicit OlesnitskiyVHoareSortSimpleMergeSTL(const InType &in); + + private: + static int HoarePartition(std::vector &array, int left, int right); + static void HoareQuickSort(std::vector &array, int left, int right); + static void SimpleMerge(const std::vector &source, std::vector &destination, size_t left, size_t middle, + size_t right); + + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + std::vector data_; +}; + +} // namespace olesnitskiy_v_hoare_sort_simple_merge diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md new file mode 100644 index 0000000000..bf80c9c329 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -0,0 +1,103 @@ +# Сортировка Хоара с простым слиянием — STL + +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** STL +- **Вариант:** 13 + +## 1. Контекст + +STL-версия использует `std::thread` для параллельной сортировки блоков и +последующего слияния независимых пар диапазонов. В отличие от OMP и TBB, число +рабочих потоков выбирается внутри реализации через +`std::thread::hardware_concurrency`. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq = 0.0058254364 s`. + +## 3. Базовый алгоритм + +Массив делится на блоки по 64 элемента. Каждый блок сортируется quicksort Хоара, +после чего уровни простого слияния выполняются параллельно по независимым парам +диапазонов. + +## 4. Схема распараллеливания + +Количество потоков выбирается как `min(task_count, hardware_concurrency)`. Если +`hardware_concurrency()` вернул `0`, используется fallback `2`. На тестовой +машине `nproc` вернул `12`, поэтому в результатах конфигурация обозначена как +`auto (12)`. + +`RunInThreads` распределяет задачи циклически: `task_index += thread_count`. +Локальные буферы представлены стековыми переменными lambda, общий +`merged_data` безопасен, потому что каждая task пишет только свой диапазон. +`atomic` и `mutex` не используются, так как общей изменяемой скалярной +структуры нет. + +Фрагмент создания и ожидания потоков: + +```cpp +std::vector threads; +threads.reserve(thread_count); + +for (size_t thread_index = 0; thread_index < thread_count; ++thread_index) { + threads.emplace_back([thread_index, thread_count, task_count, &function]() { + for (size_t task_index = thread_index; task_index < task_count; + task_index += thread_count) { + function(task_index); + } + }); +} + +for (auto &thread : threads) { + thread.join(); +} +``` + +`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь +пул рабочих потоков, а затем дождаться завершения каждого. Если делать `join` +сразу после `emplace_back`, выполнение стало бы последовательным по потокам. + +## 5. Детали реализации + +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в +`data_`. `RunImpl` сортирует блоки и сливает уровни. `PostProcessingImpl` +проверяет сортировку и записывает результат в `GetOutput()` только при успехе. + +## 6. Проверка корректности + +STL-backend добавлен в общий список задач функционального теста, а эталон +строится через `std::ranges::sort`. Свежий запуск +`build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed. +ALL отдельно прошел под `mpirun -np 2`: 15 passed. + +## 7. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Baseline TaskRun:** `0.0058254364 s` +- **Baseline pipeline:** `0.0068995056 s` +- **Число повторов:** 5 по умолчанию + +Переменная `PPC_NUM_THREADS` в STL-коде не читается. + +## 8. Результаты + +- workers: auto (12 на тестовой машине); time: 0.0047718214 s; speedup: 1.221; + efficiency: 0.102; notes: `TaskRun`; `hardware_concurrency()`. +- workers: auto (12 на тестовой машине); time: 0.0055675704 s; speedup: 1.239; + efficiency: 0.103; notes: `pipeline`; env=1 не влияет. +- workers: auto (12 на тестовой машине); time: 0.0081357990 s; speedup: 0.848; + efficiency: 0.071; notes: `pipeline`; env=4 не влияет. + +## 9. Выводы + +STL-версия имеет те же фазы, что TBB: независимая сортировка блоков и +независимое слияние. На `N=100000` получено `0.0047718214 s`, speedup `1.221`. +Efficiency низкая (`0.102`), потому что реализация автоматически создает до +`hardware_concurrency()` рабочих потоков, а объем работы мал для такого числа +workers. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/src/ops_stl.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/src/ops_stl.cpp new file mode 100644 index 0000000000..8b68857b39 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/src/ops_stl.cpp @@ -0,0 +1,190 @@ +#include "olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp" + +#include +#include +#include +#include +#include +#include + +#include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" + +namespace olesnitskiy_v_hoare_sort_simple_merge { + +namespace { + +constexpr size_t kBlockSize = 64; + +size_t GetThreadCount(size_t task_count) { + if (task_count == 0) { + return 0; + } + + const unsigned int hardware_threads = std::thread::hardware_concurrency(); + const size_t available_threads = hardware_threads == 0 ? 2 : static_cast(hardware_threads); + return std::min(task_count, available_threads); +} + +template +void RunInThreads(size_t task_count, Function function) { + const size_t thread_count = GetThreadCount(task_count); + if (thread_count <= 1) { + for (size_t task_index = 0; task_index < task_count; ++task_index) { + function(task_index); + } + return; + } + + std::vector threads; + threads.reserve(thread_count); + + for (size_t thread_index = 0; thread_index < thread_count; ++thread_index) { + threads.emplace_back([thread_index, thread_count, task_count, &function]() { + for (size_t task_index = thread_index; task_index < task_count; task_index += thread_count) { + function(task_index); + } + }); + } + + for (auto &thread : threads) { + thread.join(); + } +} + +} // namespace + +OlesnitskiyVHoareSortSimpleMergeSTL::OlesnitskiyVHoareSortSimpleMergeSTL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = {}; +} + +int OlesnitskiyVHoareSortSimpleMergeSTL::HoarePartition(std::vector &array, int left, int right) { + const int pivot = array[left + ((right - left) / 2)]; + int i = left - 1; + int j = right + 1; + + while (true) { + ++i; + while (array[i] < pivot) { + ++i; + } + + --j; + while (array[j] > pivot) { + --j; + } + + if (i >= j) { + return j; + } + + std::swap(array[i], array[j]); + } +} + +void OlesnitskiyVHoareSortSimpleMergeSTL::HoareQuickSort(std::vector &array, int left, int right) { + std::stack> stack; + stack.emplace(left, right); + + while (!stack.empty()) { + auto [current_left, current_right] = stack.top(); + stack.pop(); + + if (current_left >= current_right) { + continue; + } + + const int middle = HoarePartition(array, current_left, current_right); + + if ((middle - current_left) > (current_right - (middle + 1))) { + stack.emplace(current_left, middle); + stack.emplace(middle + 1, current_right); + } else { + stack.emplace(middle + 1, current_right); + stack.emplace(current_left, middle); + } + } +} + +void OlesnitskiyVHoareSortSimpleMergeSTL::SimpleMerge(const std::vector &source, std::vector &destination, + size_t left, size_t middle, size_t right) { + size_t left_index = left; + size_t right_index = middle; + 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++]; + } +} + +bool OlesnitskiyVHoareSortSimpleMergeSTL::ValidationImpl() { + return !GetInput().empty(); +} + +bool OlesnitskiyVHoareSortSimpleMergeSTL::PreProcessingImpl() { + data_ = GetInput(); + GetOutput().clear(); + return true; +} + +bool OlesnitskiyVHoareSortSimpleMergeSTL::RunImpl() { + if (data_.size() <= 1) { + return true; + } + + const size_t size = data_.size(); + const size_t block_count = (size + kBlockSize - 1) / kBlockSize; + + RunInThreads(block_count, [this, size](size_t block_index) { + size_t block_start = block_index * kBlockSize; + size_t block_end = std::min(block_start + kBlockSize, size); + if ((block_end - block_start) > 1) { + HoareQuickSort(data_, static_cast(block_start), static_cast(block_end - 1)); + } + }); + + for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + std::vector merged_data(size); + const size_t merge_count = (size + (2 * merge_width) - 1) / (2 * merge_width); + + RunInThreads(merge_count, [this, size, merge_width, &merged_data](size_t merge_index) { + size_t left = merge_index * 2 * merge_width; + size_t middle = std::min(left + merge_width, size); + size_t right = std::min(left + (2 * merge_width), size); + + if (middle < right) { + SimpleMerge(data_, merged_data, left, middle, right); + } else { + std::copy(data_.begin() + static_cast(left), data_.begin() + static_cast(right), + merged_data.begin() + static_cast(left)); + } + }); + + data_.swap(merged_data); + } + + return true; +} + +bool OlesnitskiyVHoareSortSimpleMergeSTL::PostProcessingImpl() { + const bool is_sorted = std::ranges::is_sorted(data_); + if (is_sorted) { + GetOutput() = data_; + } + return is_sorted; +} + +} // namespace olesnitskiy_v_hoare_sort_simple_merge diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md new file mode 100644 index 0000000000..b2f870697a --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md @@ -0,0 +1,104 @@ +# Сортировка Хоара с простым слиянием — TBB + +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** TBB +- **Вариант:** 13 + +## 1. Контекст + +TBB-версия переносит две независимые фазы алгоритма на `oneapi::tbb`: +сортировку блоков и слияние соседних отсортированных диапазонов. Эта реализация +проверяет, насколько эффективно планировщик TBB распределяет работу для входа +`N=100000`. + +## 2. Постановка задачи + +- **Входные данные:** непустой объект `std::vector`. +- **Выходные данные:** отсортированный по неубыванию `std::vector`. +- **Baseline:** последовательная версия с временем `T_seq = 0.0058254364 s`. + +## 3. Базовый алгоритм + +Массив `int` сортируется блоками по 64 элемента, затем соседние отсортированные +диапазоны объединяются простым слиянием. Локальная сортировка использует ту же +схему Хоара, что и SEQ: pivot из середины, два индекса и обмен до пересечения. + +## 4. Схема распараллеливания + +Используется `oneapi::tbb::parallel_for` с +`oneapi::tbb::blocked_range` для диапазона номеров блоков и диапазона +номеров слияний. Grainsize и partitioner явно не задаются, поэтому применяются +значения по умолчанию. + +Конкуренция ограничивается раннером через +`tbb::global_control(max_allowed_parallelism, ppc::util::GetNumThreads())`. +`GetNumThreads` читает `PPC_NUM_THREADS`. + +Фрагмент TBB-части: + +```cpp +oneapi::tbb::parallel_for( + oneapi::tbb::blocked_range(0, block_count), + [this, size](const auto &range) { + for (size_t block_index = range.begin(); block_index != range.end(); + ++block_index) { + size_t block_start = block_index * kBlockSize; + size_t block_end = std::min(block_start + kBlockSize, size); + if ((block_end - block_start) > 1) { + HoareQuickSort(data_, static_cast(block_start), + static_cast(block_end - 1)); + } + } +}); + +for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + std::vector merged_data(size); + const size_t merge_count = (size + (2 * merge_width) - 1) / (2 * merge_width); + + oneapi::tbb::parallel_for( + oneapi::tbb::blocked_range(0, merge_count), + [this, size, merge_width, &merged_data](const auto &range) { +``` + +## 5. Детали реализации + +`ValidationImpl`, `PreProcessingImpl`, `RunImpl` и `PostProcessingImpl` +расположены в `tbb/src/ops_tbb.cpp`. Сортировка блоков пишет в +непересекающиеся отрезки `data_`. Слияние пишет в непересекающиеся отрезки +`merged_data`, читая `data_`. `data_.swap` выполняется после завершения +`parallel_for`. + +## 6. Проверка корректности + +Функциональный тест сравнивает результат с `std::ranges::sort`. Запуск текущего +`build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 passed. +ALL отдельно прошел под `mpirun -np 2`: 15 passed. + +## 7. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Baseline TaskRun:** `0.0058254364 s` +- **Baseline pipeline:** `0.0068995056 s` +- **Число повторов:** 5 по умолчанию + +## 8. Результаты + +- threads: 1; time: 0.0024417256 s; speedup: 2.386; efficiency: 2.386; notes: + `TaskRun`, `PPC_NUM_THREADS=1`. +- threads: 2; time: 0.0014976288 s; speedup: 3.889; efficiency: 1.945; notes: + `TaskRun`, `PPC_NUM_THREADS=2`. +- threads: 4; time: 0.0011774682 s; speedup: 4.947; efficiency: 1.237; notes: + `TaskRun`, `PPC_NUM_THREADS=4`. +- threads: 1; time: 0.0067520042 s; speedup: 1.022; efficiency: 1.022; notes: + `pipeline`, `PPC_NUM_THREADS=1`. +- threads: 4; time: 0.0026312252 s; speedup: 2.622; efficiency: 0.656; notes: + `pipeline`, `PPC_NUM_THREADS=4`. + +## 9. Выводы + +В измерениях TBB показал лучший результат среди потоковых backend-ов: +`0.0011774682 s` при 4 потоках, speedup `4.947`. Числа относятся к +`N=100000`; для других размеров нужны отдельные замеры. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/functional/main.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/functional/main.cpp index baaa13d6e8..3db9829d2c 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/functional/main.cpp +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/functional/main.cpp @@ -9,9 +9,11 @@ #include #include +#include "olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/omp/include/ops_omp.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/seq/include/ops_seq.hpp" +#include "olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.hpp" #include "util/include/func_test_util.hpp" #include "util/include/util.hpp" @@ -79,7 +81,11 @@ const auto kTestTasksList = std::tuple_cat(ppc::util::AddFuncTask( kTestParam, PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge), + ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge), ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge), + ppc::util::AddFuncTask( kTestParam, PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge)); const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/performance/main.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/performance/main.cpp index a03581e836..197ef4f209 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/performance/main.cpp +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/performance/main.cpp @@ -4,9 +4,11 @@ #include #include +#include "olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/common/include/common.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/omp/include/ops_omp.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/seq/include/ops_seq.hpp" +#include "olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp" #include "olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.hpp" #include "util/include/perf_test_util.hpp" @@ -45,7 +47,8 @@ namespace { const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( + OlesnitskiyVHoareSortSimpleMergeSTL, OlesnitskiyVHoareSortSimpleMergeTBB, + OlesnitskiyVHoareSortSimpleMergeALL>( PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge); const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks);