From 81551339c1423d17d9208027b361bbd8864f84e2 Mon Sep 17 00:00:00 2001 From: milka Date: Sun, 17 May 2026 00:02:07 +0000 Subject: [PATCH 1/8] rep_1 --- .../all/include/ops_all.hpp | 35 +++ .../all/src/ops_all.cpp | 276 ++++++++++++++++++ .../stl/include/ops_stl.hpp | 32 ++ .../stl/src/ops_stl.cpp | 190 ++++++++++++ .../tbb/include/ops_tbb.hpp | 32 ++ .../tbb/src/ops_tbb.cpp | 162 ++++++++++ .../tests/functional/main.cpp | 9 + .../tests/performance/main.cpp | 7 +- 8 files changed, 742 insertions(+), 1 deletion(-) create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/all/include/ops_all.hpp create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/all/src/ops_all.cpp create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/include/ops_stl.hpp create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/src/ops_stl.cpp create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.hpp create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/src/ops_tbb.cpp 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/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/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/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/include/ops_tbb.hpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.hpp new file mode 100644 index 0000000000..39d5b89af3 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.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 OlesnitskiyVHoareSortSimpleMergeTBB : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kTBB; + } + explicit OlesnitskiyVHoareSortSimpleMergeTBB(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/tbb/src/ops_tbb.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/src/ops_tbb.cpp new file mode 100644 index 0000000000..a52c53bd22 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/src/ops_tbb.cpp @@ -0,0 +1,162 @@ +#include "olesnitskiy_v_hoare_sort_simple_merge/tbb/include/ops_tbb.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; + +} // namespace + +OlesnitskiyVHoareSortSimpleMergeTBB::OlesnitskiyVHoareSortSimpleMergeTBB(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = {}; +} + +int OlesnitskiyVHoareSortSimpleMergeTBB::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 OlesnitskiyVHoareSortSimpleMergeTBB::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 OlesnitskiyVHoareSortSimpleMergeTBB::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 OlesnitskiyVHoareSortSimpleMergeTBB::ValidationImpl() { + return !GetInput().empty(); +} + +bool OlesnitskiyVHoareSortSimpleMergeTBB::PreProcessingImpl() { + data_ = GetInput(); + GetOutput().clear(); + return true; +} + +bool OlesnitskiyVHoareSortSimpleMergeTBB::RunImpl() { + if (data_.size() <= 1) { + return true; + } + + const size_t size = data_.size(); + const size_t block_count = (size + kBlockSize - 1) / kBlockSize; + + 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) { + for (size_t merge_index = range.begin(); merge_index != range.end(); ++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 OlesnitskiyVHoareSortSimpleMergeTBB::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/tests/functional/main.cpp b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tests/functional/main.cpp index 04bef90668..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,12 @@ #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" @@ -77,6 +80,12 @@ const std::array kTestParam = { 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), + 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 304d49a8d4..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,12 @@ #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" namespace olesnitskiy_v_hoare_sort_simple_merge { @@ -43,7 +46,9 @@ TEST_P(OlesnitskiyVRunPerfTestsThreads, RunPerfModes) { namespace { const auto kAllPerfTasks = - ppc::util::MakeAllPerfTasks( + ppc::util::MakeAllPerfTasks( PPC_SETTINGS_olesnitskiy_v_hoare_sort_simple_merge); const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); From 56e38b7f97aea0bf60366e13d97905a58b4bc3ff Mon Sep 17 00:00:00 2001 From: milka Date: Sun, 17 May 2026 00:41:05 +0000 Subject: [PATCH 2/8] rep_2 --- .../all/report.md | 78 ++++++++++++ .../omp/report.md | 81 +++++++++++++ .../report.md | 114 ++++++++++++++++++ .../seq/report.md | 84 +++++++++++++ .../stl/report.md | 73 +++++++++++ .../tbb/report.md | 72 +++++++++++ 6 files changed, 502 insertions(+) create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md create mode 100644 tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md 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..389f781597 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md @@ -0,0 +1,78 @@ +# Отчет ALL: MPI + STL для сортировки Хоара с простым слиянием + +## Контекст и базовый алгоритм + +ALL-версия объединяет MPI между процессами и `std::thread` внутри процесса. Корневой rank хранит входной массив, данные +распределяются между rank-ами, каждый rank локально сортирует свой фрагмент STL-схемой, затем фрагменты собираются на +rank 0 и последовательно досливаются ([`all/src/ops_all.cpp`](src/ops_all.cpp#L227)). + +## Межпроцессная схема + +`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов ([`all/src/ops_all.cpp`](src/ops_all.cpp#L228)). +`BuildDistribution` делит `total_size` почти поровну: первые `remainder` rank-ов получают на один элемент больше +([`all/src/ops_all.cpp`](src/ops_all.cpp#L64)). `MPI_Scatterv` отправляет локальные куски, `MPI_Gatherv` собирает +отсортированные куски на rank 0, затем `BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast` +([`all/src/ops_all.cpp`](src/ops_all.cpp#L246), [`all/src/ops_all.cpp`](src/ops_all.cpp#L256), +[`all/src/ops_all.cpp`](src/ops_all.cpp#L264)). + +Фрагмент, [`all/src/ops_all.cpp`](src/ops_all.cpp#L238): распределение, scatter/gather и broadcast. + +```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); +} +``` + +## Внутрипроцессная схема + +Локальная сортировка повторяет STL-версию: `RunInThreads`, блоки по 64, затем уровни слияния +([`all/src/ops_all.cpp`](src/ops_all.cpp#L167)). Количество потоков внутри rank-а выбирается по +`std::thread::hardware_concurrency()` и числу локальных задач ([`all/src/ops_all.cpp`](src/ops_all.cpp#L20)). + +## Конфигурация ranks × threads и цена синхронизации + +Для ALL нужно указывать `ranks`, `threads_per_rank`, `total_workers = ranks * threads_per_rank`. В коде явного +`MPI_Barrier` нет; обязательная цена синхронизации возникает на коллективных операциях `MPI_Scatterv`, `MPI_Gatherv` и +`MPI_Bcast`. Дополнительный `MPI_Barrier` добавляет тестовый listener раннера после каждого теста +([`modules/runners/src/runners.cpp`](../../../modules/runners/src/runners.cpp#L23)). + +## Детали pipeline и корректность + +`ValidationImpl` проверяет непустой вход ([`all/src/ops_all.cpp`](src/ops_all.cpp#L217)). `PreProcessingImpl` копирует +вход в `data_` ([`all/src/ops_all.cpp`](src/ops_all.cpp#L221)). `RunImpl` выполняет MPI-распределение, локальную +сортировку, сбор и broadcast ([`all/src/ops_all.cpp`](src/ops_all.cpp#L227)). `PostProcessingImpl` проверяет +`std::ranges::is_sorted` и записывает выход ([`all/src/ops_all.cpp`](src/ops_all.cpp#L268)). В исходном функциональном +тесте ALL-backend зарегистрирован ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L88)). + +## Результаты + +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Performance-вход `N=100000` +([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Framework выполняет 5 повторов по умолчанию +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). + +| backend | ranks | threads_per_rank | total_workers | time | speedup | efficiency | notes | +|---|---:|---:|---:|---:|---:|---:|---| +| all | 1 | 12 | 12 | 0.0126647332 s | 0.460 | 0.038 | `mpirun -np 1`, `PPC_NUM_THREADS=1`, local STL auto-threads | +| all | 2 | 12 | 24 | 0.0088037862 s | 0.662 | 0.028 | `mpirun -np 2`, local STL auto-threads | +| all | 4 | 12 | 48 | 0.0043261284 s | 1.347 | 0.028 | `mpirun -np 4`, local STL auto-threads | +| all | 4 | 12 | 48 | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, `mpirun -np 4`, local STL auto-threads | + +## Выводы + +ALL-версия добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на rank 0 +([`all/src/ops_all.cpp`](src/ops_all.cpp#L204)). На 4 rank-ах получено `0.0043261284 s`, speedup `1.347`; efficiency +`0.028`, потому что каждый rank дополнительно запускает до 12 STL-потоков, а коммуникации остаются обязательными. 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..5f187e0e2c --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md @@ -0,0 +1,81 @@ +# Отчет OMP: сортировка Хоара с простым слиянием + +## Контекст и базовый алгоритм + +Вход и выход: `std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). Последовательное ядро: +разбиение Хоара и quicksort по локальным диапазонам ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L21)). После локальной +сортировки блоков по 64 элемента выполняется попарное простое слияние до полного массива +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L115)). Средняя сложность локальной quicksort-части `O(n log n)`, слияния по +уровням дают дополнительный линейный проход на каждом уровне. + +## Схема распараллеливания + +OpenMP используется в двух `parallel for`: сортировка независимых блоков и слияние независимых пар блоков +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L119), [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L130)). В директивах стоит +`default(none)`, поэтому все shared-переменные перечислены явно: `size`, `data`, `merged_data`, `merge_width`. Индексы +циклов и временные векторы являются private по области видимости тела цикла. Reduction не нужен: нет общей скалярной +агрегации. Явный `schedule` не задан, значит применяется runtime-default OpenMP; для равномерных блоков по 64 элемента +это соответствует идее статического распределения без дополнительных вычислений планировщика. Барьер OpenMP в конце +каждого `parallel for` нужен перед `data.swap(merged_data)`. + +Фрагмент, [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L119): параллельные блоки пишут в непересекающиеся диапазоны. + +```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); +``` + +## Детали pipeline + +`ValidationImpl` проверяет непустой вход ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L100)). `PreProcessingImpl` копирует +вход в `data_` и очищает выход ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L104)). `RunImpl` сортирует блоки и выполняет +итеративное слияние ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L110)). `PostProcessingImpl` проверяет `data_` через +`std::ranges::is_sorted` и только после этого записывает `GetOutput()` ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L156)). + +## Проверка отсутствия гонок + +При сортировке блок `block_start..block_end` не пересекается с соседним, потому что шаг равен `kBlockSize` +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L120)). При слиянии каждый поток пишет в `merged_data[left, right)`, где `left` +идет с шагом `2 * merge_width` ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L131)). Общий `data` на фазе слияния только +читается, а `data.swap(merged_data)` выполняется после завершения параллельного цикла +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L150)). + +## Корректность и среда + +Эталон в функциональных тестах строится `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). +Запуск `PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge*'` +прошел: 75 tests, 60 passed, 15 ALL-tests skipped вне `mpirun`. ALL отдельно прошел под `mpirun -np 2`: 15 passed. +Performance-вход: 100000 случайных чисел ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). + +## Результаты + +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Потоки задавались через +`PPC_NUM_THREADS`; раннер ограничивает TBB этой же переменной, а OpenMP использует ее через окружение сборки/запуска. +Framework выполняет 5 повторов по умолчанию ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). + +| threads | time | speedup | efficiency | notes | +|---:|---:|---:|---:|---| +| 1 | 0.2042550788 s | 0.029 | 0.029 | `TaskRun`, `N=100000` | +| 2 | 0.2022266690 s | 0.029 | 0.014 | `TaskRun`, `N=100000` | +| 4 | 0.1880041056 s | 0.031 | 0.008 | `TaskRun`, `N=100000` | +| 1 | 0.1145523454 s | 0.060 | 0.060 | `pipeline`, `N=100000` | +| 4 | 0.1193738506 s | 0.058 | 0.014 | `pipeline`, `N=100000` | + +## Выводы + +На данном размере входа OpenMP-версия медленнее baseline: лучший замер `0.1880041056 s` при 4 потоках дает speedup +`0.031`. Причина подтверждается кодом: блок `kBlockSize=64` создает много мелких задач и временных векторов при слиянии +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L115), [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L136)). 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..c7ae123d85 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -0,0 +1,114 @@ +# Сводный отчет: сортировка Хоара с простым слиянием + +## Введение и постановка + +Работа сравнивает реализации сортировки `std::vector` по неубыванию для backend-ов `seq`, `omp`, `tbb`, `stl` и +`all`. Единые типы входа/выхода заданы в [`common/include/common.hpp`](common/include/common.hpp#L11). Корректность +определяется совпадением с результатом `std::ranges::sort` в функциональном тесте +([`tests/functional/main.cpp`](tests/functional/main.cpp#L35)). + +## Единая методика эксперимента + +Performance-тест генерирует `N=100000` случайных `int` из диапазона `[-1000000, 1000000]` +([`tests/performance/main.cpp`](tests/performance/main.cpp#L20)). Использован `TaskRun`; режим выбирается в раннере +([`modules/util/include/perf_test_util.hpp`](../../modules/util/include/perf_test_util.hpp#L87)). Speedup считался как +`T_seq / T_backend`, efficiency как `speedup / workers`. Baseline: `seq`, `PPC_NUM_THREADS=1`, +`T_seq = 0.0058254364 s`. Для OMP/TBB применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно ограничивается +`tbb::global_control` ([`modules/runners/src/runners.cpp`](../../modules/runners/src/runners.cpp#L150)). STL и ALL +берут число локальных потоков из `std::thread::hardware_concurrency()`; на этой машине `nproc = 12`. +Число повторов по умолчанию равно 5 ([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L21)). +Ограничение методики: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` +([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L62)), а вход +создается заново для каждого gtest-параметра через `std::random_device` ([`tests/performance/main.cpp`](tests/performance/main.cpp#L24)). +Поэтому дополнительно сняты pipeline-замеры, где каждый повтор проходит полный pipeline. + +Среда: свежая Release-сборка `build_olesnitskiy`, `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`; эти флаги видны в +[`build_olesnitskiy/compile_commands.json`](../../build_olesnitskiy/compile_commands.json). Число повторов задается framework-ом +`Perf::TaskRun`; отчет опирается на напечатанные строки `backend:task_run:time`. + +## Сводка корректности + +Источник функциональных тестов регистрирует все пять backend-ов +([`tests/functional/main.cpp`](tests/functional/main.cpp#L80)). Запуск +`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*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 --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*'` +прошел 15/15 ALL-тестов. + +## Агрегированные результаты + +| backend | time | speedup | efficiency | notes | +|---|---:|---:|---:|---| +| seq | 0.0058254364 s | 1.000 | 1.000 | `TaskRun`, `N=100000`, baseline | +| omp, 1 thread | 0.2042550788 s | 0.029 | 0.029 | много мелких блоков `kBlockSize=64` | +| omp, 2 threads | 0.2022266690 s | 0.029 | 0.014 | `PPC_NUM_THREADS=2` | +| omp, 4 threads | 0.1880041056 s | 0.031 | 0.008 | лучший OMP-замер | +| tbb, 1 worker | 0.0024417256 s | 2.386 | 2.386 | `blocked_range`, `parallel_for` | +| tbb, 2 workers | 0.0014976288 s | 3.889 | 1.945 | `PPC_NUM_THREADS=2` | +| tbb, 4 workers | 0.0011774682 s | 4.947 | 1.237 | лучший потоковый backend | +| stl, 12 workers | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`, `nproc=12` | +| all, 1 rank x 12 threads | 0.0126647332 s | 0.460 | 0.038 | total_workers=12 | +| all, 2 ranks x 12 threads | 0.0088037862 s | 0.662 | 0.028 | total_workers=24 | +| all, 4 ranks x 12 threads | 0.0043261284 s | 1.347 | 0.028 | total_workers=48 | + +Дополнительные pipeline-замеры: + +| backend | time | speedup | efficiency | notes | +|---|---:|---:|---:|---| +| seq | 0.0068995056 s | 1.000 | 1.000 | `pipeline`, baseline | +| omp, 1 thread | 0.1145523454 s | 0.060 | 0.060 | `pipeline` | +| omp, 4 threads | 0.1193738506 s | 0.058 | 0.014 | `pipeline` | +| stl, 12 workers | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-threads | +| tbb, 1 worker | 0.0067520042 s | 1.022 | 1.022 | `pipeline` | +| tbb, 4 workers | 0.0026312252 s | 2.622 | 0.656 | `pipeline` | +| all, 4 ranks x 12 threads | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, total_workers=48 | + +## Интерпретация различий + +`seq` сортирует весь массив одним quicksort и служит baseline ([`seq/src/ops_seq.cpp`](seq/src/ops_seq.cpp#L75)). +`omp` создает параллельные области для множества блоков по 64 и выделяет временные векторы при слиянии +([`omp/src/ops_omp.cpp`](omp/src/ops_omp.cpp#L115), [`omp/src/ops_omp.cpp`](omp/src/ops_omp.cpp#L136)); на `N=100000` +измеренный speedup меньше 1. `tbb` использует `parallel_for(blocked_range)` для блоков и слияний +([`tbb/src/ops_tbb.cpp`](tbb/src/ops_tbb.cpp#L117)); при 4 workers получено `4.947x`. `stl` вручную создает и join-ит +потоки ([`stl/src/ops_stl.cpp`](stl/src/ops_stl.cpp#L38)); при 12 workers получено `1.221x`, но efficiency `0.102`. +`all` добавляет `MPI_Scatterv/Gatherv/Bcast` и финальное слияние на rank 0 +([`all/src/ops_all.cpp`](all/src/ops_all.cpp#L246)); на 4 rank-ах время `0.0043261284 s`, но efficiency `0.028`. + +## Репродуцируемость + +Сборка: + +```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' +``` + +## Заключение + +Для измеренного размера `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 из-за накладных расходов. + +## Источники + +1. OpenMP API Specification 5.2, разделы data-sharing и schedule: https://www.openmp.org/spec-html/5.2/openmp.html +2. oneTBB `parallel_for`: https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-9/parallel-for.html +3. oneTBB `blocked_range`: https://oneapi-spec.uxlfoundation.org/specifications/oneapi/latest/elements/onetbb/source/algorithms/blocked_ranges/blocked_range_cls +4. MPI Standard, коллективные операции: https://www.mpi-forum.org/docs/ +5. `std::thread::join`: https://en.cppreference.com/w/cpp/thread/thread/join.html 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..59d9b84c83 --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md @@ -0,0 +1,84 @@ +# Отчет SEQ: сортировка Хоара с простым слиянием + +## Контекст и постановка + +Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода заданы как `std::vector` в +[`common/include/common.hpp`](../common/include/common.hpp#L11). Последовательная версия использует разбиение Хоара и +итеративный quicksort: опорный элемент берется из середины диапазона, индексы двигаются навстречу, элементы меняются +местами до пересечения ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18)). Средняя сложность сортировки: `O(n log n)`, +худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в неблагоприятном случае. Инвариант разбиения: после возврата +`j` элементы слева от границы не больше опорной группы, справа не меньше; рекурсивные подзадачи заменены стеком +диапазонов ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L42)). + +Фрагмент реализации, [`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18): выбор pivot и схема Хоара. + +```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]); + } +} +``` + +## Детали реализации + +`ValidationImpl` принимает только непустой вход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L66)). `PreProcessingImpl` +копирует вход в выходной буфер, чтобы сортировать результат без изменения `GetInput()` ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L70)). +`RunImpl` пропускает массивы размера `0/1`, затем вызывает `HoareQuickSort` и проверяет `std::ranges::is_sorted` +([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L75)). `PostProcessingImpl` повторно проверяет непустой и отсортированный +выход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L86)). + +## Проверка корректности + +Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и сравнивает его с выходом задачи +([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Набор содержит 15 сценариев: один элемент, +дубликаты, обратный порядок, отрицательные значения, границу блока 64 и `int`-пределы +([`tests/functional/main.cpp`](../tests/functional/main.cpp#L59)). Запуск +`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge*'` +прошел: 75 тестов, 60 passed и 15 ALL-тестов skipped вне `mpirun`. Отдельный запуск +`mpirun -np 2 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*'` +прошел 15/15 ALL-тестов. + +## Экспериментальная среда + +Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и oneTBB видны в +[`build_olesnitskiy/compile_commands.json`](../../../build_olesnitskiy/compile_commands.json). Размер performance-набора: 100000 случайных +`int` из `[-1000000, 1000000]` ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Время печатает +`Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` ([`modules/util/include/perf_test_util.hpp`](../../../modules/util/include/perf_test_util.hpp#L87)). +Число повторов по умолчанию равно 5 ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). +Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L62)), а вход для +каждого gtest-параметра генерируется заново через `std::random_device` ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L24)). + +## Результаты + +Baseline измерен командой: +`PPC_NUM_THREADS=1 ./build_olesnitskiy/bin/ppc_perf_tests --gtest_filter='*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled'`. + +| backend | time | speedup | efficiency | notes | +|---|---:|---:|---:|---| +| seq | 0.0058254364 s | 1.000 | 1.000 | `N=100000`, `TaskRun`, `PPC_NUM_THREADS=1` | +| seq | 0.0068995056 s | 1.000 | 1.000 | `N=100000`, `pipeline`, `PPC_NUM_THREADS=1` | + +## Выводы + +Последовательная версия является baseline для speedup. Корректность подтверждена сравнением с `std::ranges::sort`; +производительность подтверждена `TaskRun` и `pipeline`-замерами на `N=100000`. 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..84b11dfb1b --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -0,0 +1,73 @@ +# Отчет STL: сортировка Хоара с простым слиянием + +## Контекст и базовый алгоритм + +STL-версия использует `std::thread`: массив делится на блоки по 64 элемента, каждый блок сортируется quicksort Хоара, +после чего уровни простого слияния выполняются параллельно по независимым парам диапазонов +([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L16), [`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L143)). Вход и выход имеют тип +`std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). + +## Разбиение, буферы и синхронизация + +Количество потоков выбирается как `min(task_count, hardware_concurrency)`; если `hardware_concurrency()` вернул `0`, +используется fallback `2` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L18)). `RunInThreads` распределяет задачи циклически: +`task_index += thread_count` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L41)). Локальные буферы представлены стековыми +переменными lambda, общий `merged_data` безопасен, потому что каждый task пишет только свой диапазон +([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L163)). `atomic` и `mutex` не используются, потому что общей изменяемой +скалярной структуры нет. + +`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь пул рабочих потоков, а затем дождаться +завершения каждого. Если делать `join` сразу после `emplace_back`, выполнение стало бы последовательным по потокам. +Семантика `join` подтверждена cppreference: завершение потока синхронизируется с успешным возвратом из `join` +([cppreference `std::thread::join`](https://en.cppreference.com/w/cpp/thread/thread/join.html)). + +Фрагмент, [`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L38): создание всех потоков и последующий join. + +```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(); +} +``` + +## Детали pipeline + +`ValidationImpl` проверяет непустой вход ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L133)). `PreProcessingImpl` копирует +вход в `data_` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L137)). `RunImpl` сортирует блоки и сливает уровни +([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L143)). `PostProcessingImpl` проверяет сортировку и записывает результат в +`GetOutput()` только при успехе ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L182)). + +## Корректность и среда + +В исходном тесте STL-backend добавлен в общий список задач ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L84)), +а эталон строится через `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Свежий +запуск `build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел под +`mpirun -np 2`: 15 passed. + +## Результаты + +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Performance-вход описан в +[`tests/performance/main.cpp`](../tests/performance/main.cpp#L20). Framework выполняет 5 повторов по умолчанию +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). + +| threads | time | speedup | efficiency | notes | +|---:|---:|---:|---:|---| +| 12 | 0.0047718214 s | 1.221 | 0.102 | `std::thread::hardware_concurrency()`, `nproc=12`, `TaskRun` | +| 12 | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-threads | +| 12 | 0.0081357990 s | 0.848 | 0.071 | `pipeline`, `PPC_NUM_THREADS=4`, фактически auto-threads | + +## Выводы + +По коду STL-версия имеет те же фазы, что TBB: независимая сортировка блоков и независимое слияние. На `N=100000` +получено `0.0047718214 s`, speedup `1.221`; efficiency низкая (`0.102`), потому что реализация создает до 12 рабочих +потоков по `hardware_concurrency()`, а объем работы мал для такого числа потоков. 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..4a7f8595fc --- /dev/null +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md @@ -0,0 +1,72 @@ +# Отчет TBB: сортировка Хоара с простым слиянием + +## Контекст и базовый алгоритм + +TBB-версия сортирует массив `int` блоками по 64 элемента, затем сливает соседние отсортированные диапазоны +([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L18), [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L127)). Локальная сортировка +использует ту же схему Хоара, что и `seq`: pivot из середины, два индекса и обмен до пересечения +([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L28)). + +## TBB-примитивы + +Используется `oneapi::tbb::parallel_for` с `oneapi::tbb::blocked_range` для диапазона индексов блоков и индексов +слияния ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L117), [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L131)). Grainsize явно +не передан, поэтому применяется значение конструктора `blocked_range` по умолчанию; partitioner также явно не указан, +значит используется стандартное разбиение `parallel_for`. Конкуренция ограничивается раннером через +`tbb::global_control(max_allowed_parallelism, ppc::util::GetNumThreads())` +([`modules/runners/src/runners.cpp`](../../../modules/runners/src/runners.cpp#L150)); `GetNumThreads` читает +`PPC_NUM_THREADS` ([`modules/util/src/util.cpp`](../../../modules/util/src/util.cpp#L23)). + +Фрагмент, [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L117): `blocked_range` задает независимые номера блоков. + +```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) { +``` + +## Детали pipeline и гонки + +`ValidationImpl`, `PreProcessingImpl`, `RunImpl`, `PostProcessingImpl` расположены в +[`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L99). Сортировка блоков пишет в непересекающиеся отрезки `data_`; слияние пишет +в непересекающиеся отрезки `merged_data`, читая `data_` ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L138)). `data_.swap` +выполняется после завершения `parallel_for` ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L148)). + +## Корректность и среда + +Функциональный тест сравнивает результат с `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). +Запуск текущего `build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел +под `mpirun -np 2`: 15 passed. Performance-вход: `N=100000` +([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). + +## Результаты + +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Framework выполняет 5 повторов +по умолчанию ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). + +| threads | time | speedup | efficiency | notes | +|---:|---:|---:|---:|---| +| 1 | 0.0024417256 s | 2.386 | 2.386 | `TaskRun`, `PPC_NUM_THREADS=1` | +| 2 | 0.0014976288 s | 3.889 | 1.945 | `TaskRun`, `PPC_NUM_THREADS=2` | +| 4 | 0.0011774682 s | 4.947 | 1.237 | `TaskRun`, `PPC_NUM_THREADS=4` | +| 1 | 0.0067520042 s | 1.022 | 1.022 | `pipeline`, `PPC_NUM_THREADS=1` | +| 4 | 0.0026312252 s | 2.622 | 0.656 | `pipeline`, `PPC_NUM_THREADS=4` | + +## Выводы + +В измерениях TBB показал лучший результат среди потоковых backend-ов: `0.0011774682 s` +при 4 потоках, speedup `4.947`. Числа относятся к `N=100000` и фиксируются командой `ppc_perf_tests`; вывод не +распространяется на другие размеры без дополнительных замеров. From 1f1975c76438ccb31a6941ad4feea73590bc0bfc Mon Sep 17 00:00:00 2001 From: milka Date: Sun, 17 May 2026 00:50:34 +0000 Subject: [PATCH 3/8] rep_3 --- .../report.md | 9 +++++---- .../stl/report.md | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md index c7ae123d85..144a9ee054 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -14,8 +14,9 @@ Performance-тест генерирует `N=100000` случайных `int` и ([`modules/util/include/perf_test_util.hpp`](../../modules/util/include/perf_test_util.hpp#L87)). Speedup считался как `T_seq / T_backend`, efficiency как `speedup / workers`. Baseline: `seq`, `PPC_NUM_THREADS=1`, `T_seq = 0.0058254364 s`. Для OMP/TBB применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно ограничивается -`tbb::global_control` ([`modules/runners/src/runners.cpp`](../../modules/runners/src/runners.cpp#L150)). STL и ALL -берут число локальных потоков из `std::thread::hardware_concurrency()`; на этой машине `nproc = 12`. +`tbb::global_control` ([`modules/runners/src/runners.cpp`](../../modules/runners/src/runners.cpp#L150)). STL и локальная +часть ALL берут число потоков из `std::thread::hardware_concurrency()`; на этой машине `nproc = 12`, поэтому такие +строки обозначены как auto workers. Число повторов по умолчанию равно 5 ([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L21)). Ограничение методики: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` ([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L62)), а вход @@ -46,7 +47,7 @@ Performance-тест генерирует `N=100000` случайных `int` и | tbb, 1 worker | 0.0024417256 s | 2.386 | 2.386 | `blocked_range`, `parallel_for` | | tbb, 2 workers | 0.0014976288 s | 3.889 | 1.945 | `PPC_NUM_THREADS=2` | | tbb, 4 workers | 0.0011774682 s | 4.947 | 1.237 | лучший потоковый backend | -| stl, 12 workers | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`, `nproc=12` | +| stl, auto workers (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`, `PPC_NUM_THREADS` не используется STL-кодом | | all, 1 rank x 12 threads | 0.0126647332 s | 0.460 | 0.038 | total_workers=12 | | all, 2 ranks x 12 threads | 0.0088037862 s | 0.662 | 0.028 | total_workers=24 | | all, 4 ranks x 12 threads | 0.0043261284 s | 1.347 | 0.028 | total_workers=48 | @@ -58,7 +59,7 @@ Performance-тест генерирует `N=100000` случайных `int` и | seq | 0.0068995056 s | 1.000 | 1.000 | `pipeline`, baseline | | omp, 1 thread | 0.1145523454 s | 0.060 | 0.060 | `pipeline` | | omp, 4 threads | 0.1193738506 s | 0.058 | 0.014 | `pipeline` | -| stl, 12 workers | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-threads | +| stl, auto workers (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-workers | | tbb, 1 worker | 0.0067520042 s | 1.022 | 1.022 | `pipeline` | | tbb, 4 workers | 0.0026312252 s | 2.622 | 0.656 | `pipeline` | | all, 4 ranks x 12 threads | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, total_workers=48 | diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md index 84b11dfb1b..cc8c246556 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -9,8 +9,10 @@ STL-версия использует `std::thread`: массив делится ## Разбиение, буферы и синхронизация -Количество потоков выбирается как `min(task_count, hardware_concurrency)`; если `hardware_concurrency()` вернул `0`, -используется fallback `2` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L18)). `RunInThreads` распределяет задачи циклически: +Количество потоков выбирается внутри реализации как `min(task_count, hardware_concurrency)`; переменная окружения +`PPC_NUM_THREADS` в STL-коде не читается. Если `hardware_concurrency()` вернул `0`, используется fallback `2` +([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L18)). На тестовой машине `nproc` вернул `12`, поэтому в таблице конфигурация +обозначена как `auto (12)`, а не как вручную заданные 12 потоков. `RunInThreads` распределяет задачи циклически: `task_index += thread_count` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L41)). Локальные буферы представлены стековыми переменными lambda, общий `merged_data` безопасен, потому что каждый task пишет только свой диапазон ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L163)). `atomic` и `mutex` не используются, потому что общей изменяемой @@ -60,14 +62,14 @@ Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.00689950 [`tests/performance/main.cpp`](../tests/performance/main.cpp#L20). Framework выполняет 5 повторов по умолчанию ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). -| threads | time | speedup | efficiency | notes | -|---:|---:|---:|---:|---| -| 12 | 0.0047718214 s | 1.221 | 0.102 | `std::thread::hardware_concurrency()`, `nproc=12`, `TaskRun` | -| 12 | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-threads | -| 12 | 0.0081357990 s | 0.848 | 0.071 | `pipeline`, `PPC_NUM_THREADS=4`, фактически auto-threads | +| workers | time | speedup | efficiency | notes | +|---|---:|---:|---:|---| +| auto (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `TaskRun`; число workers взято из `hardware_concurrency()` | +| auto (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; запуск с `PPC_NUM_THREADS=1`, но STL-код его не использует | +| auto (12 на тестовой машине) | 0.0081357990 s | 0.848 | 0.071 | `pipeline`; запуск с `PPC_NUM_THREADS=4`, но STL-код его не использует | ## Выводы По коду STL-версия имеет те же фазы, что TBB: независимая сортировка блоков и независимое слияние. На `N=100000` -получено `0.0047718214 s`, speedup `1.221`; efficiency низкая (`0.102`), потому что реализация создает до 12 рабочих -потоков по `hardware_concurrency()`, а объем работы мал для такого числа потоков. +получено `0.0047718214 s`, speedup `1.221`; efficiency низкая (`0.102`), потому что реализация автоматически создает +до `hardware_concurrency()` рабочих потоков, а объем работы мал для такого числа workers. From e7cef7a40cfa818378ca021664b0395ac9aee79d Mon Sep 17 00:00:00 2001 From: milka Date: Sun, 17 May 2026 14:26:48 +0000 Subject: [PATCH 4/8] rep_4 --- .../all/report.md | 5 +- .../omp/report.md | 16 +++-- .../report.md | 38 ++++++---- .../seq/report.md | 70 +++++++++++-------- .../stl/report.md | 5 +- .../tbb/report.md | 8 ++- 6 files changed, 87 insertions(+), 55 deletions(-) diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md index 389f781597..c1cde0e745 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md @@ -8,7 +8,8 @@ rank 0 и последовательно досливаются ([`all/src/ops_a ## Межпроцессная схема -`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов ([`all/src/ops_all.cpp`](src/ops_all.cpp#L228)). +`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов +([`all/src/ops_all.cpp`](src/ops_all.cpp#L228)). `BuildDistribution` делит `total_size` почти поровну: первые `remainder` rank-ов получают на один элемент больше ([`all/src/ops_all.cpp`](src/ops_all.cpp#L64)). `MPI_Scatterv` отправляет локальные куски, `MPI_Gatherv` собирает отсортированные куски на rank 0, затем `BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast` @@ -65,7 +66,7 @@ Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.00689950 ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). | backend | ranks | threads_per_rank | total_workers | time | speedup | efficiency | notes | -|---|---:|---:|---:|---:|---:|---:|---| +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | | all | 1 | 12 | 12 | 0.0126647332 s | 0.460 | 0.038 | `mpirun -np 1`, `PPC_NUM_THREADS=1`, local STL auto-threads | | all | 2 | 12 | 24 | 0.0088037862 s | 0.662 | 0.028 | `mpirun -np 2`, local STL auto-threads | | all | 4 | 12 | 48 | 0.0043261284 s | 1.347 | 0.028 | `mpirun -np 4`, local STL auto-threads | diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md index 5f187e0e2c..bde912a694 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md @@ -2,8 +2,9 @@ ## Контекст и базовый алгоритм -Вход и выход: `std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). Последовательное ядро: -разбиение Хоара и quicksort по локальным диапазонам ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L21)). После локальной +Вход и выход: `std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). +Последовательное ядро: разбиение Хоара и quicksort по локальным диапазонам +([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L21)). После локальной сортировки блоков по 64 элемента выполняется попарное простое слияние до полного массива ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L115)). Средняя сложность локальной quicksort-части `O(n log n)`, слияния по уровням дают дополнительный линейный проход на каждом уровне. @@ -55,8 +56,10 @@ OpenMP используется в двух `parallel for`: сортировка ## Корректность и среда -Эталон в функциональных тестах строится `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). -Запуск `PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge*'` +Эталон в функциональных тестах строится `std::ranges::sort` +([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). +Запуск `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. Performance-вход: 100000 случайных чисел ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). @@ -64,10 +67,11 @@ Performance-вход: 100000 случайных чисел ([`tests/performance/ Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Потоки задавались через `PPC_NUM_THREADS`; раннер ограничивает TBB этой же переменной, а OpenMP использует ее через окружение сборки/запуска. -Framework выполняет 5 повторов по умолчанию ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). +Framework выполняет 5 повторов по умолчанию +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). | threads | time | speedup | efficiency | notes | -|---:|---:|---:|---:|---| +| ---: | ---: | ---: | ---: | --- | | 1 | 0.2042550788 s | 0.029 | 0.029 | `TaskRun`, `N=100000` | | 2 | 0.2022266690 s | 0.029 | 0.014 | `TaskRun`, `N=100000` | | 4 | 0.1880041056 s | 0.031 | 0.008 | `TaskRun`, `N=100000` | diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md index 144a9ee054..360c1d99ac 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -17,14 +17,17 @@ Performance-тест генерирует `N=100000` случайных `int` и `tbb::global_control` ([`modules/runners/src/runners.cpp`](../../modules/runners/src/runners.cpp#L150)). STL и локальная часть ALL берут число потоков из `std::thread::hardware_concurrency()`; на этой машине `nproc = 12`, поэтому такие строки обозначены как auto workers. -Число повторов по умолчанию равно 5 ([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L21)). +Число повторов по умолчанию равно 5 +([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L21)). Ограничение методики: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` ([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L62)), а вход -создается заново для каждого gtest-параметра через `std::random_device` ([`tests/performance/main.cpp`](tests/performance/main.cpp#L24)). +создается заново для каждого gtest-параметра через `std::random_device` +([`tests/performance/main.cpp`](tests/performance/main.cpp#L24)). Поэтому дополнительно сняты pipeline-замеры, где каждый повтор проходит полный pipeline. Среда: свежая Release-сборка `build_olesnitskiy`, `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`; эти флаги видны в -[`build_olesnitskiy/compile_commands.json`](../../build_olesnitskiy/compile_commands.json). Число повторов задается framework-ом +[`build_olesnitskiy/compile_commands.json`](../../build_olesnitskiy/compile_commands.json). +Число повторов задается framework-ом `Perf::TaskRun`; отчет опирается на напечатанные строки `backend:task_run:time`. ## Сводка корректности @@ -33,13 +36,14 @@ Performance-тест генерирует `N=100000` случайных `int` и ([`tests/functional/main.cpp`](tests/functional/main.cpp#L80)). Запуск `PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*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 --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*'` +`mpirun -np 2 ./build_olesnitskiy/bin/ppc_func_tests` +с фильтром `*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*` прошел 15/15 ALL-тестов. ## Агрегированные результаты | backend | time | speedup | efficiency | notes | -|---|---:|---:|---:|---| +| --- | ---: | ---: | ---: | --- | | seq | 0.0058254364 s | 1.000 | 1.000 | `TaskRun`, `N=100000`, baseline | | omp, 1 thread | 0.2042550788 s | 0.029 | 0.029 | много мелких блоков `kBlockSize=64` | | omp, 2 threads | 0.2022266690 s | 0.029 | 0.014 | `PPC_NUM_THREADS=2` | @@ -55,7 +59,7 @@ Performance-тест генерирует `N=100000` случайных `int` и Дополнительные pipeline-замеры: | backend | time | speedup | efficiency | notes | -|---|---:|---:|---:|---| +| --- | ---: | ---: | ---: | --- | | seq | 0.0068995056 s | 1.000 | 1.000 | `pipeline`, baseline | | omp, 1 thread | 0.1145523454 s | 0.060 | 0.060 | `pipeline` | | omp, 4 threads | 0.1193738506 s | 0.058 | 0.014 | `pipeline` | @@ -88,16 +92,23 @@ 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*' +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' +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' ``` ## Заключение @@ -109,7 +120,8 @@ workers и MPI-обменов. OMP на этом входе проигрывае ## Источники 1. OpenMP API Specification 5.2, разделы data-sharing и schedule: https://www.openmp.org/spec-html/5.2/openmp.html -2. oneTBB `parallel_for`: https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-9/parallel-for.html -3. oneTBB `blocked_range`: https://oneapi-spec.uxlfoundation.org/specifications/oneapi/latest/elements/onetbb/source/algorithms/blocked_ranges/blocked_range_cls +2. oneTBB `parallel_for`: + https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-9/parallel-for.html +3. oneTBB `blocked_range`: https://oneapi-spec.uxlfoundation.org/ 4. MPI Standard, коллективные операции: https://www.mpi-forum.org/docs/ 5. `std::thread::join`: https://en.cppreference.com/w/cpp/thread/thread/join.html diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md index 59d9b84c83..fcb87ddb5b 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md @@ -2,15 +2,18 @@ ## Контекст и постановка -Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода заданы как `std::vector` в -[`common/include/common.hpp`](../common/include/common.hpp#L11). Последовательная версия использует разбиение Хоара и -итеративный quicksort: опорный элемент берется из середины диапазона, индексы двигаются навстречу, элементы меняются -местами до пересечения ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18)). Средняя сложность сортировки: `O(n log n)`, -худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в неблагоприятном случае. Инвариант разбиения: после возврата -`j` элементы слева от границы не больше опорной группы, справа не меньше; рекурсивные подзадачи заменены стеком +Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода заданы как +`std::vector` в [`common/include/common.hpp`](../common/include/common.hpp#L11). +Последовательная версия использует разбиение Хоара и итеративный quicksort: опорный +элемент берется из середины диапазона, индексы двигаются навстречу, элементы меняются +местами до пересечения ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18)). Средняя сложность +сортировки: `O(n log n)`, худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в +неблагоприятном случае. Инвариант разбиения: после возврата `j` элементы слева от границы +не больше опорной группы, справа не меньше; рекурсивные подзадачи заменены стеком диапазонов ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L42)). -Фрагмент реализации, [`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18): выбор pivot и схема Хоара. +Фрагмент реализации, [`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18): +выбор pivot и схема Хоара. ```cpp int OlesnitskiyVHoareSortSimpleMergeSEQ::HoarePartition(std::vector &values, int left, int right) { @@ -40,45 +43,54 @@ int OlesnitskiyVHoareSortSimpleMergeSEQ::HoarePartition(std::vector &values ## Детали реализации -`ValidationImpl` принимает только непустой вход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L66)). `PreProcessingImpl` -копирует вход в выходной буфер, чтобы сортировать результат без изменения `GetInput()` ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L70)). -`RunImpl` пропускает массивы размера `0/1`, затем вызывает `HoareQuickSort` и проверяет `std::ranges::is_sorted` -([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L75)). `PostProcessingImpl` повторно проверяет непустой и отсортированный -выход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L86)). +`ValidationImpl` принимает только непустой вход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L66)). +`PreProcessingImpl` копирует вход в выходной буфер, чтобы сортировать результат без изменения +`GetInput()` ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L70)). `RunImpl` пропускает массивы +размера `0/1`, затем вызывает `HoareQuickSort` и проверяет `std::ranges::is_sorted` +([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L75)). `PostProcessingImpl` повторно проверяет +непустой и отсортированный выход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L86)). ## Проверка корректности -Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и сравнивает его с выходом задачи -([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Набор содержит 15 сценариев: один элемент, -дубликаты, обратный порядок, отрицательные значения, границу блока 64 и `int`-пределы +Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и сравнивает его +с выходом задачи ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). +Набор содержит 15 сценариев: один элемент, дубликаты, обратный порядок, отрицательные +значения, границу блока 64 и `int`-пределы ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L59)). Запуск -`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge*'` +`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 --gtest_filter='*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*'` -прошел 15/15 ALL-тестов. +`mpirun -np 2 ./build_olesnitskiy/bin/ppc_func_tests` +с фильтром `*olesnitskiy_v_hoare_sort_simple_merge_all_enabled*` прошел 15/15 ALL-тестов. ## Экспериментальная среда -Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и oneTBB видны в -[`build_olesnitskiy/compile_commands.json`](../../../build_olesnitskiy/compile_commands.json). Размер performance-набора: 100000 случайных -`int` из `[-1000000, 1000000]` ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Время печатает -`Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` ([`modules/util/include/perf_test_util.hpp`](../../../modules/util/include/perf_test_util.hpp#L87)). -Число повторов по умолчанию равно 5 ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). +Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и oneTBB +видны в [`build_olesnitskiy/compile_commands.json`](../../../build_olesnitskiy/compile_commands.json). +Размер performance-набора: 100000 случайных `int` из `[-1000000, 1000000]` +([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Время печатает +`Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` +([`modules/util/include/perf_test_util.hpp`](../../../modules/util/include/perf_test_util.hpp#L87)). +Число повторов по умолчанию равно 5 +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L62)), а вход для -каждого gtest-параметра генерируется заново через `std::random_device` ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L24)). +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L62)), +а вход для каждого gtest-параметра генерируется заново через `std::random_device` +([`tests/performance/main.cpp`](../tests/performance/main.cpp#L24)). ## Результаты Baseline измерен командой: -`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=1 ./build_olesnitskiy/bin/ppc_perf_tests` +с фильтром `*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled`. | backend | time | speedup | efficiency | notes | -|---|---:|---:|---:|---| +| --- | ---: | ---: | ---: | --- | | seq | 0.0058254364 s | 1.000 | 1.000 | `N=100000`, `TaskRun`, `PPC_NUM_THREADS=1` | | seq | 0.0068995056 s | 1.000 | 1.000 | `N=100000`, `pipeline`, `PPC_NUM_THREADS=1` | ## Выводы -Последовательная версия является baseline для speedup. Корректность подтверждена сравнением с `std::ranges::sort`; -производительность подтверждена `TaskRun` и `pipeline`-замерами на `N=100000`. +Последовательная версия является baseline для speedup. Корректность подтверждена сравнением +с `std::ranges::sort`; производительность подтверждена `TaskRun` и `pipeline`-замерами на +`N=100000`. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md index cc8c246556..23dba8791d 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -51,7 +51,8 @@ for (auto &thread : threads) { ## Корректность и среда -В исходном тесте STL-backend добавлен в общий список задач ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L84)), +В исходном тесте STL-backend добавлен в общий список задач +([`tests/functional/main.cpp`](../tests/functional/main.cpp#L84)), а эталон строится через `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Свежий запуск `build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел под `mpirun -np 2`: 15 passed. @@ -63,7 +64,7 @@ Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.00689950 ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). | workers | time | speedup | efficiency | notes | -|---|---:|---:|---:|---| +| --- | ---: | ---: | ---: | --- | | auto (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `TaskRun`; число workers взято из `hardware_concurrency()` | | auto (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; запуск с `PPC_NUM_THREADS=1`, но STL-код его не использует | | auto (12 на тестовой машине) | 0.0081357990 s | 0.848 | 0.071 | `pipeline`; запуск с `PPC_NUM_THREADS=4`, но STL-код его не использует | diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md index 4a7f8595fc..88da9cd053 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md @@ -47,7 +47,8 @@ for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { ## Корректность и среда -Функциональный тест сравнивает результат с `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). +Функциональный тест сравнивает результат с `std::ranges::sort` +([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Запуск текущего `build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел под `mpirun -np 2`: 15 passed. Performance-вход: `N=100000` ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). @@ -55,10 +56,11 @@ for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { ## Результаты Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Framework выполняет 5 повторов -по умолчанию ([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). +по умолчанию +([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). | threads | time | speedup | efficiency | notes | -|---:|---:|---:|---:|---| +| ---: | ---: | ---: | ---: | --- | | 1 | 0.0024417256 s | 2.386 | 2.386 | `TaskRun`, `PPC_NUM_THREADS=1` | | 2 | 0.0014976288 s | 3.889 | 1.945 | `TaskRun`, `PPC_NUM_THREADS=2` | | 4 | 0.0011774682 s | 4.947 | 1.237 | `TaskRun`, `PPC_NUM_THREADS=4` | From b07e9f2c1e793b6ef71730646b5945648c1ebcae Mon Sep 17 00:00:00 2001 From: milka-246 <105885233+milka-246@users.noreply.github.com> Date: Mon, 18 May 2026 00:27:32 +0300 Subject: [PATCH 5/8] Add STL operations header to main.cpp --- .../tests/functional/main.cpp | 1 + 1 file changed, 1 insertion(+) 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 d0ba3a1890..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 @@ -13,6 +13,7 @@ #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" From 233a773f05b0876484816d05f8a35c414da5deb6 Mon Sep 17 00:00:00 2001 From: milka Date: Mon, 18 May 2026 13:42:53 +0000 Subject: [PATCH 6/8] fix ref 321 --- tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md | 4 ++-- tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md index 360c1d99ac..1f048544eb 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -51,7 +51,7 @@ Performance-тест генерирует `N=100000` случайных `int` и | tbb, 1 worker | 0.0024417256 s | 2.386 | 2.386 | `blocked_range`, `parallel_for` | | tbb, 2 workers | 0.0014976288 s | 3.889 | 1.945 | `PPC_NUM_THREADS=2` | | tbb, 4 workers | 0.0011774682 s | 4.947 | 1.237 | лучший потоковый backend | -| stl, auto workers (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`, `PPC_NUM_THREADS` не используется STL-кодом | +| stl, auto workers (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`; без env | | all, 1 rank x 12 threads | 0.0126647332 s | 0.460 | 0.038 | total_workers=12 | | all, 2 ranks x 12 threads | 0.0088037862 s | 0.662 | 0.028 | total_workers=24 | | all, 4 ranks x 12 threads | 0.0043261284 s | 1.347 | 0.028 | total_workers=48 | @@ -63,7 +63,7 @@ Performance-тест генерирует `N=100000` случайных `int` и | seq | 0.0068995056 s | 1.000 | 1.000 | `pipeline`, baseline | | omp, 1 thread | 0.1145523454 s | 0.060 | 0.060 | `pipeline` | | omp, 4 threads | 0.1193738506 s | 0.058 | 0.014 | `pipeline` | -| stl, auto workers (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`, `PPC_NUM_THREADS=1`, фактически auto-workers | +| stl, auto workers (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; auto | | tbb, 1 worker | 0.0067520042 s | 1.022 | 1.022 | `pipeline` | | tbb, 4 workers | 0.0026312252 s | 2.622 | 0.656 | `pipeline` | | all, 4 ranks x 12 threads | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, total_workers=48 | diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md index 23dba8791d..9000b0739d 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -65,9 +65,9 @@ Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.00689950 | workers | time | speedup | efficiency | notes | | --- | ---: | ---: | ---: | --- | -| auto (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `TaskRun`; число workers взято из `hardware_concurrency()` | -| auto (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; запуск с `PPC_NUM_THREADS=1`, но STL-код его не использует | -| auto (12 на тестовой машине) | 0.0081357990 s | 0.848 | 0.071 | `pipeline`; запуск с `PPC_NUM_THREADS=4`, но STL-код его не использует | +| auto (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `TaskRun`; `hardware_concurrency()` | +| auto (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; env=1 не влияет | +| auto (12 на тестовой машине) | 0.0081357990 s | 0.848 | 0.071 | `pipeline`; env=4 не влияет | ## Выводы From 1eebdfd859884f9805fe444307c9ebf550bc2d7e Mon Sep 17 00:00:00 2001 From: milka Date: Tue, 19 May 2026 08:14:02 +0000 Subject: [PATCH 7/8] fix_rep_md --- .../all/report.md | 99 ++++++---- .../omp/report.md | 111 +++++++----- .../report.md | 169 ++++++++++-------- .../seq/report.md | 105 ++++++----- .../stl/report.md | 96 ++++++---- .../tbb/report.md | 93 ++++++---- 6 files changed, 395 insertions(+), 278 deletions(-) diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md index c1cde0e745..7021f39756 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md @@ -2,21 +2,26 @@ ## Контекст и базовый алгоритм -ALL-версия объединяет MPI между процессами и `std::thread` внутри процесса. Корневой rank хранит входной массив, данные -распределяются между rank-ами, каждый rank локально сортирует свой фрагмент STL-схемой, затем фрагменты собираются на -rank 0 и последовательно досливаются ([`all/src/ops_all.cpp`](src/ops_all.cpp#L227)). +ALL-версия объединяет MPI между процессами и `std::thread` внутри процесса. +Корневой rank хранит входной массив, данные распределяются между rank-ами, +каждый rank локально сортирует свой фрагмент STL-схемой, затем фрагменты +собираются на rank 0 и последовательно досливаются +(`all/src/ops_all.cpp:227`). ## Межпроцессная схема `MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов -([`all/src/ops_all.cpp`](src/ops_all.cpp#L228)). -`BuildDistribution` делит `total_size` почти поровну: первые `remainder` rank-ов получают на один элемент больше -([`all/src/ops_all.cpp`](src/ops_all.cpp#L64)). `MPI_Scatterv` отправляет локальные куски, `MPI_Gatherv` собирает -отсортированные куски на rank 0, затем `BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast` -([`all/src/ops_all.cpp`](src/ops_all.cpp#L246), [`all/src/ops_all.cpp`](src/ops_all.cpp#L256), -[`all/src/ops_all.cpp`](src/ops_all.cpp#L264)). - -Фрагмент, [`all/src/ops_all.cpp`](src/ops_all.cpp#L238): распределение, scatter/gather и broadcast. +(`all/src/ops_all.cpp:228`). `BuildDistribution` делит +`total_size` почти поровну: первые `remainder` rank-ов получают на один элемент +больше (`all/src/ops_all.cpp:64`). `MPI_Scatterv` отправляет +локальные куски, `MPI_Gatherv` собирает отсортированные куски на rank 0, затем +`BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast` +(`all/src/ops_all.cpp:246`, +`all/src/ops_all.cpp:256`, +`all/src/ops_all.cpp:264`). + +Фрагмент, `all/src/ops_all.cpp:238`: распределение, +scatter/gather и broadcast. ```cpp std::vector chunk_sizes(static_cast(mpi_size)); @@ -27,8 +32,9 @@ 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); +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); @@ -40,40 +46,57 @@ if (mpi_rank == 0) { ## Внутрипроцессная схема -Локальная сортировка повторяет STL-версию: `RunInThreads`, блоки по 64, затем уровни слияния -([`all/src/ops_all.cpp`](src/ops_all.cpp#L167)). Количество потоков внутри rank-а выбирается по -`std::thread::hardware_concurrency()` и числу локальных задач ([`all/src/ops_all.cpp`](src/ops_all.cpp#L20)). +Локальная сортировка повторяет STL-версию: `RunInThreads`, блоки по 64, затем +уровни слияния (`all/src/ops_all.cpp:167`). Количество +потоков внутри rank-а выбирается по `std::thread::hardware_concurrency()` и +числу локальных задач (`all/src/ops_all.cpp:20`). ## Конфигурация ranks × threads и цена синхронизации -Для ALL нужно указывать `ranks`, `threads_per_rank`, `total_workers = ranks * threads_per_rank`. В коде явного -`MPI_Barrier` нет; обязательная цена синхронизации возникает на коллективных операциях `MPI_Scatterv`, `MPI_Gatherv` и -`MPI_Bcast`. Дополнительный `MPI_Barrier` добавляет тестовый listener раннера после каждого теста -([`modules/runners/src/runners.cpp`](../../../modules/runners/src/runners.cpp#L23)). +Для ALL нужно указывать `ranks`, `threads_per_rank`, `total_workers = ranks * +threads_per_rank`. В коде явного `MPI_Barrier` нет; обязательная цена +синхронизации возникает на коллективных операциях `MPI_Scatterv`, `MPI_Gatherv` +и `MPI_Bcast`. Дополнительный `MPI_Barrier` добавляет тестовый listener раннера +после каждого теста +(`modules/runners/src/runners.cpp:23`). ## Детали pipeline и корректность -`ValidationImpl` проверяет непустой вход ([`all/src/ops_all.cpp`](src/ops_all.cpp#L217)). `PreProcessingImpl` копирует -вход в `data_` ([`all/src/ops_all.cpp`](src/ops_all.cpp#L221)). `RunImpl` выполняет MPI-распределение, локальную -сортировку, сбор и broadcast ([`all/src/ops_all.cpp`](src/ops_all.cpp#L227)). `PostProcessingImpl` проверяет -`std::ranges::is_sorted` и записывает выход ([`all/src/ops_all.cpp`](src/ops_all.cpp#L268)). В исходном функциональном -тесте ALL-backend зарегистрирован ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L88)). +`ValidationImpl` проверяет непустой вход +(`all/src/ops_all.cpp:217`). `PreProcessingImpl` копирует +вход в `data_` (`all/src/ops_all.cpp:221`). `RunImpl` +выполняет MPI-распределение, локальную сортировку, сбор и broadcast +(`all/src/ops_all.cpp:227`). `PostProcessingImpl` проверяет +`std::ranges::is_sorted` и записывает выход +(`all/src/ops_all.cpp:268`). В исходном функциональном тесте +ALL-backend зарегистрирован +(`tests/functional/main.cpp:88`). ## Результаты -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Performance-вход `N=100000` -([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Framework выполняет 5 повторов по умолчанию -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). - -| backend | ranks | threads_per_rank | total_workers | time | speedup | efficiency | notes | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | -| all | 1 | 12 | 12 | 0.0126647332 s | 0.460 | 0.038 | `mpirun -np 1`, `PPC_NUM_THREADS=1`, local STL auto-threads | -| all | 2 | 12 | 24 | 0.0088037862 s | 0.662 | 0.028 | `mpirun -np 2`, local STL auto-threads | -| all | 4 | 12 | 48 | 0.0043261284 s | 1.347 | 0.028 | `mpirun -np 4`, local STL auto-threads | -| all | 4 | 12 | 48 | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, `mpirun -np 4`, local STL auto-threads | +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 +s`. Performance-вход `N=100000` +(`tests/performance/main.cpp:20`). Framework +выполняет 5 повторов по умолчанию +(`modules/performance/include/performance.hpp:21`). + +- 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. ## Выводы -ALL-версия добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на rank 0 -([`all/src/ops_all.cpp`](src/ops_all.cpp#L204)). На 4 rank-ах получено `0.0043261284 s`, speedup `1.347`; efficiency -`0.028`, потому что каждый rank дополнительно запускает до 12 STL-потоков, а коммуникации остаются обязательными. +ALL-версия добавляет стоимость `Scatterv/Gatherv/Bcast` и финальное слияние на +rank 0 (`all/src/ops_all.cpp:204`). На 4 rank-ах получено +`0.0043261284 s`, speedup `1.347`; efficiency `0.028`, потому что каждый rank +дополнительно запускает до 12 STL-потоков, а коммуникации остаются +обязательными. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md index bde912a694..07aa54a371 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md @@ -2,38 +2,48 @@ ## Контекст и базовый алгоритм -Вход и выход: `std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). +Вход и выход: `std::vector` +(`common/include/common.hpp:11`). Последовательное ядро: разбиение Хоара и quicksort по локальным диапазонам -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L21)). После локальной -сортировки блоков по 64 элемента выполняется попарное простое слияние до полного массива -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L115)). Средняя сложность локальной quicksort-части `O(n log n)`, слияния по -уровням дают дополнительный линейный проход на каждом уровне. +(`omp/src/ops_omp.cpp:21`). После локальной сортировки +блоков по 64 элемента выполняется попарное простое слияние до полного массива +(`omp/src/ops_omp.cpp:115`). Средняя сложность локальной +quicksort-части `O(n log n)`, слияния по уровням дают дополнительный линейный +проход на каждом уровне. ## Схема распараллеливания -OpenMP используется в двух `parallel for`: сортировка независимых блоков и слияние независимых пар блоков -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L119), [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L130)). В директивах стоит -`default(none)`, поэтому все shared-переменные перечислены явно: `size`, `data`, `merged_data`, `merge_width`. Индексы -циклов и временные векторы являются private по области видимости тела цикла. Reduction не нужен: нет общей скалярной -агрегации. Явный `schedule` не задан, значит применяется runtime-default OpenMP; для равномерных блоков по 64 элемента -это соответствует идее статического распределения без дополнительных вычислений планировщика. Барьер OpenMP в конце +OpenMP используется в двух `parallel for`: сортировка независимых блоков и +слияние независимых пар блоков (`omp/src/ops_omp.cpp:119`, +`omp/src/ops_omp.cpp:130`). В директивах стоит +`default(none)`, поэтому все shared-переменные перечислены явно: `size`, `data`, +`merged_data`, `merge_width`. Индексы циклов и временные векторы являются +private по области видимости тела цикла. Reduction не нужен: нет общей скалярной +агрегации. Явный `schedule` не задан, значит применяется runtime-default OpenMP; +для равномерных блоков по 64 элемента это соответствует идее статического +распределения без дополнительных вычислений планировщика. Барьер OpenMP в конце каждого `parallel for` нужен перед `data.swap(merged_data)`. -Фрагмент, [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L119): параллельные блоки пишут в непересекающиеся диапазоны. +Фрагмент, `omp/src/ops_omp.cpp:119`: параллельные блоки +пишут в непересекающиеся диапазоны. ```cpp #pragma omp parallel for default(none) shared(size, data) - for (std::size_t block_start = 0; block_start < size; block_start += kBlockSize) { + 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)); + HoareQuickSort(data, static_cast(block_start), + static_cast(block_end - 1)); } } - for (std::size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { + 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) +#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); @@ -41,45 +51,56 @@ OpenMP используется в двух `parallel for`: сортировка ## Детали pipeline -`ValidationImpl` проверяет непустой вход ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L100)). `PreProcessingImpl` копирует -вход в `data_` и очищает выход ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L104)). `RunImpl` сортирует блоки и выполняет -итеративное слияние ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L110)). `PostProcessingImpl` проверяет `data_` через -`std::ranges::is_sorted` и только после этого записывает `GetOutput()` ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L156)). +`ValidationImpl` проверяет непустой вход +(`omp/src/ops_omp.cpp:100`). `PreProcessingImpl` копирует +вход в `data_` и очищает выход (`omp/src/ops_omp.cpp:104`). +`RunImpl` сортирует блоки и выполняет итеративное слияние +(`omp/src/ops_omp.cpp:110`). `PostProcessingImpl` проверяет +`data_` через `std::ranges::is_sorted` и только после этого записывает +`GetOutput()` (`omp/src/ops_omp.cpp:156`). ## Проверка отсутствия гонок -При сортировке блок `block_start..block_end` не пересекается с соседним, потому что шаг равен `kBlockSize` -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L120)). При слиянии каждый поток пишет в `merged_data[left, right)`, где `left` -идет с шагом `2 * merge_width` ([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L131)). Общий `data` на фазе слияния только -читается, а `data.swap(merged_data)` выполняется после завершения параллельного цикла -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L150)). +При сортировке блок `block_start..block_end` не пересекается с соседним, потому +что шаг равен `kBlockSize` (`omp/src/ops_omp.cpp:120`). При +слиянии каждый поток пишет в `merged_data[left, right)`, где `left` идет с шагом +`2 * merge_width` (`omp/src/ops_omp.cpp:131`). Общий `data` +на фазе слияния только читается, а `data.swap(merged_data)` выполняется после +завершения параллельного цикла (`omp/src/ops_omp.cpp:150`). ## Корректность и среда Эталон в функциональных тестах строится `std::ranges::sort` -([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). -Запуск `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. -Performance-вход: 100000 случайных чисел ([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). +(`tests/functional/main.cpp:35`). Запуск +`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. Performance-вход: 100000 случайных чисел +(`tests/performance/main.cpp:20`). ## Результаты -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Потоки задавались через -`PPC_NUM_THREADS`; раннер ограничивает TBB этой же переменной, а OpenMP использует ее через окружение сборки/запуска. -Framework выполняет 5 повторов по умолчанию -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). - -| threads | time | speedup | efficiency | notes | -| ---: | ---: | ---: | ---: | --- | -| 1 | 0.2042550788 s | 0.029 | 0.029 | `TaskRun`, `N=100000` | -| 2 | 0.2022266690 s | 0.029 | 0.014 | `TaskRun`, `N=100000` | -| 4 | 0.1880041056 s | 0.031 | 0.008 | `TaskRun`, `N=100000` | -| 1 | 0.1145523454 s | 0.060 | 0.060 | `pipeline`, `N=100000` | -| 4 | 0.1193738506 s | 0.058 | 0.014 | `pipeline`, `N=100000` | +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 +s`. Потоки задавались через `PPC_NUM_THREADS`; раннер ограничивает TBB этой же +переменной, а OpenMP использует ее через окружение сборки/запуска. Framework +выполняет 5 повторов по умолчанию +(`modules/performance/include/performance.hpp:21`). + +- 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`. ## Выводы -На данном размере входа OpenMP-версия медленнее baseline: лучший замер `0.1880041056 s` при 4 потоках дает speedup -`0.031`. Причина подтверждается кодом: блок `kBlockSize=64` создает много мелких задач и временных векторов при слиянии -([`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L115), [`omp/src/ops_omp.cpp`](src/ops_omp.cpp#L136)). +На данном размере входа OpenMP-версия медленнее baseline: лучший замер +`0.1880041056 s` при 4 потоках дает speedup `0.031`. Причина подтверждается +кодом: блок `kBlockSize=64` создает много мелких задач и временных векторов при +слиянии (`omp/src/ops_omp.cpp:115`, +`omp/src/ops_omp.cpp:136`). diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md index 1f048544eb..7680e67881 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -2,82 +2,106 @@ ## Введение и постановка -Работа сравнивает реализации сортировки `std::vector` по неубыванию для backend-ов `seq`, `omp`, `tbb`, `stl` и -`all`. Единые типы входа/выхода заданы в [`common/include/common.hpp`](common/include/common.hpp#L11). Корректность -определяется совпадением с результатом `std::ranges::sort` в функциональном тесте -([`tests/functional/main.cpp`](tests/functional/main.cpp#L35)). +Работа сравнивает реализации сортировки `std::vector` по неубыванию для +backend-ов `seq`, `omp`, `tbb`, `stl` и `all`. Единые типы входа/выхода заданы в +`common/include/common.hpp:11`. Корректность +определяется совпадением с результатом `std::ranges::sort` в функциональном +тесте (`tests/functional/main.cpp:35`). ## Единая методика эксперимента -Performance-тест генерирует `N=100000` случайных `int` из диапазона `[-1000000, 1000000]` -([`tests/performance/main.cpp`](tests/performance/main.cpp#L20)). Использован `TaskRun`; режим выбирается в раннере -([`modules/util/include/perf_test_util.hpp`](../../modules/util/include/perf_test_util.hpp#L87)). Speedup считался как -`T_seq / T_backend`, efficiency как `speedup / workers`. Baseline: `seq`, `PPC_NUM_THREADS=1`, -`T_seq = 0.0058254364 s`. Для OMP/TBB применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно ограничивается -`tbb::global_control` ([`modules/runners/src/runners.cpp`](../../modules/runners/src/runners.cpp#L150)). STL и локальная -часть ALL берут число потоков из `std::thread::hardware_concurrency()`; на этой машине `nproc = 12`, поэтому такие -строки обозначены как auto workers. -Число повторов по умолчанию равно 5 -([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L21)). -Ограничение методики: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` -([`modules/performance/include/performance.hpp`](../../modules/performance/include/performance.hpp#L62)), а вход -создается заново для каждого gtest-параметра через `std::random_device` -([`tests/performance/main.cpp`](tests/performance/main.cpp#L24)). -Поэтому дополнительно сняты pipeline-замеры, где каждый повтор проходит полный pipeline. - -Среда: свежая Release-сборка `build_olesnitskiy`, `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`; эти флаги видны в -[`build_olesnitskiy/compile_commands.json`](../../build_olesnitskiy/compile_commands.json). -Число повторов задается framework-ом -`Perf::TaskRun`; отчет опирается на напечатанные строки `backend:task_run:time`. +Performance-тест генерирует `N=100000` случайных `int` из диапазона `[-1000000, +1000000]` (`tests/performance/main.cpp:20`). +Использован `TaskRun`; режим выбирается в раннере +(`modules/util/include/perf_test_util.hpp:87`). +Speedup считался как `T_seq / T_backend`, efficiency как `speedup / workers`. +Baseline: `seq`, `PPC_NUM_THREADS=1`, `T_seq = 0.0058254364 s`. Для OMP/TBB +применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно ограничивается +`tbb::global_control` +(`modules/runners/src/runners.cpp:150`). +STL и локальная часть ALL берут число потоков из +`std::thread::hardware_concurrency()`; на этой машине `nproc = 12`, поэтому +такие строки обозначены как auto workers. Число повторов по умолчанию равно 5 +(`modules/performance/include/performance.hpp:21`). +Ограничение методики: `TaskRun` повторяет только `Run()` после одного +`PreProcessing()` +(`modules/performance/include/performance.hpp:62`), +а вход создается заново для каждого gtest-параметра через `std::random_device` +(`tests/performance/main.cpp:24`). Поэтому +дополнительно сняты pipeline-замеры, где каждый повтор проходит полный pipeline. + +Среда: свежая Release-сборка `build_olesnitskiy`, `g++-14`, `-O3 -DNDEBUG`, +`std=gnu++23`; эти флаги видны в +`build_olesnitskiy/compile_commands.json`. +Число повторов задается framework-ом `Perf::TaskRun`; отчет опирается на +напечатанные строки `backend:task_run:time`. ## Сводка корректности Источник функциональных тестов регистрирует все пять backend-ов -([`tests/functional/main.cpp`](tests/functional/main.cpp#L80)). Запуск -`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests --gtest_filter='*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-тестов. +(`tests/functional/main.cpp:80`). Запуск +`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests +--gtest_filter='*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-тестов. ## Агрегированные результаты -| backend | time | speedup | efficiency | notes | -| --- | ---: | ---: | ---: | --- | -| seq | 0.0058254364 s | 1.000 | 1.000 | `TaskRun`, `N=100000`, baseline | -| omp, 1 thread | 0.2042550788 s | 0.029 | 0.029 | много мелких блоков `kBlockSize=64` | -| omp, 2 threads | 0.2022266690 s | 0.029 | 0.014 | `PPC_NUM_THREADS=2` | -| omp, 4 threads | 0.1880041056 s | 0.031 | 0.008 | лучший OMP-замер | -| tbb, 1 worker | 0.0024417256 s | 2.386 | 2.386 | `blocked_range`, `parallel_for` | -| tbb, 2 workers | 0.0014976288 s | 3.889 | 1.945 | `PPC_NUM_THREADS=2` | -| tbb, 4 workers | 0.0011774682 s | 4.947 | 1.237 | лучший потоковый backend | -| stl, auto workers (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `hardware_concurrency`; без env | -| all, 1 rank x 12 threads | 0.0126647332 s | 0.460 | 0.038 | total_workers=12 | -| all, 2 ranks x 12 threads | 0.0088037862 s | 0.662 | 0.028 | total_workers=24 | -| all, 4 ranks x 12 threads | 0.0043261284 s | 1.347 | 0.028 | total_workers=48 | +- 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. Дополнительные pipeline-замеры: -| backend | time | speedup | efficiency | notes | -| --- | ---: | ---: | ---: | --- | -| seq | 0.0068995056 s | 1.000 | 1.000 | `pipeline`, baseline | -| omp, 1 thread | 0.1145523454 s | 0.060 | 0.060 | `pipeline` | -| omp, 4 threads | 0.1193738506 s | 0.058 | 0.014 | `pipeline` | -| stl, auto workers (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; auto | -| tbb, 1 worker | 0.0067520042 s | 1.022 | 1.022 | `pipeline` | -| tbb, 4 workers | 0.0026312252 s | 2.622 | 0.656 | `pipeline` | -| all, 4 ranks x 12 threads | 0.0516886980 s | 0.133 | 0.003 | `pipeline`, total_workers=48 | +- 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. ## Интерпретация различий -`seq` сортирует весь массив одним quicksort и служит baseline ([`seq/src/ops_seq.cpp`](seq/src/ops_seq.cpp#L75)). -`omp` создает параллельные области для множества блоков по 64 и выделяет временные векторы при слиянии -([`omp/src/ops_omp.cpp`](omp/src/ops_omp.cpp#L115), [`omp/src/ops_omp.cpp`](omp/src/ops_omp.cpp#L136)); на `N=100000` -измеренный speedup меньше 1. `tbb` использует `parallel_for(blocked_range)` для блоков и слияний -([`tbb/src/ops_tbb.cpp`](tbb/src/ops_tbb.cpp#L117)); при 4 workers получено `4.947x`. `stl` вручную создает и join-ит -потоки ([`stl/src/ops_stl.cpp`](stl/src/ops_stl.cpp#L38)); при 12 workers получено `1.221x`, но efficiency `0.102`. -`all` добавляет `MPI_Scatterv/Gatherv/Bcast` и финальное слияние на rank 0 -([`all/src/ops_all.cpp`](all/src/ops_all.cpp#L246)); на 4 rank-ах время `0.0043261284 s`, но efficiency `0.028`. +`seq` сортирует весь массив одним quicksort и служит baseline +(`seq/src/ops_seq.cpp:75`). `omp` создает параллельные +области для множества блоков по 64 и выделяет временные векторы при слиянии +(`omp/src/ops_omp.cpp:115`, +`omp/src/ops_omp.cpp:136`); на `N=100000` измеренный +speedup меньше 1. `tbb` использует `parallel_for(blocked_range)` для блоков и +слияний (`tbb/src/ops_tbb.cpp:117`); при 4 workers +получено `4.947x`. `stl` вручную создает и join-ит потоки +(`stl/src/ops_stl.cpp:38`); при 12 workers получено +`1.221x`, но efficiency `0.102`. `all` добавляет `MPI_Scatterv/Gatherv/Bcast` и +финальное слияние на rank 0 (`all/src/ops_all.cpp:246`); +на 4 rank-ах время `0.0043261284 s`, но efficiency `0.028`. ## Репродуцируемость @@ -91,7 +115,8 @@ 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*' +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*' @@ -113,15 +138,19 @@ PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_perf_tests \ ## Заключение -Для измеренного размера `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 из-за накладных расходов. +Для измеренного размера `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 из-за накладных +расходов. ## Источники -1. OpenMP API Specification 5.2, разделы data-sharing и schedule: https://www.openmp.org/spec-html/5.2/openmp.html -2. oneTBB `parallel_for`: - https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-9/parallel-for.html -3. oneTBB `blocked_range`: https://oneapi-spec.uxlfoundation.org/ -4. MPI Standard, коллективные операции: https://www.mpi-forum.org/docs/ -5. `std::thread::join`: https://en.cppreference.com/w/cpp/thread/thread/join.html +1. OpenMP API Specification 5.2, разделы data-sharing и schedule: + https://www.openmp.org/spec-html/5.2/openmp.html +2. oneTBB `parallel_for` and `blocked_range`: + https://uxlfoundation.github.io/oneTBB/ +3. MPI Standard, коллективные операции: + https://www.mpi-forum.org/docs/ +4. `std::thread::join`: + https://en.cppreference.com/w/cpp/thread/thread/join.html diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md index fcb87ddb5b..cf71efdffa 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md @@ -2,21 +2,24 @@ ## Контекст и постановка -Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода заданы как -`std::vector` в [`common/include/common.hpp`](../common/include/common.hpp#L11). -Последовательная версия использует разбиение Хоара и итеративный quicksort: опорный -элемент берется из середины диапазона, индексы двигаются навстречу, элементы меняются -местами до пересечения ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18)). Средняя сложность -сортировки: `O(n log n)`, худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в -неблагоприятном случае. Инвариант разбиения: после возврата `j` элементы слева от границы -не больше опорной группы, справа не меньше; рекурсивные подзадачи заменены стеком -диапазонов ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L42)). - -Фрагмент реализации, [`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L18): -выбор pivot и схема Хоара. +Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода +заданы как `std::vector` в +`common/include/common.hpp:11`. +Последовательная версия использует разбиение Хоара и итеративный quicksort: +опорный элемент берется из середины диапазона, индексы двигаются навстречу, +элементы меняются местами до пересечения +(`seq/src/ops_seq.cpp:18`). Средняя сложность сортировки: +`O(n log n)`, худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в +неблагоприятном случае. Инвариант разбиения: после возврата `j` элементы слева +от границы не больше опорной группы, справа не меньше; рекурсивные подзадачи +заменены стеком диапазонов (`seq/src/ops_seq.cpp:42`). + +Фрагмент реализации, `seq/src/ops_seq.cpp:18`: выбор pivot и +схема Хоара. ```cpp -int OlesnitskiyVHoareSortSimpleMergeSEQ::HoarePartition(std::vector &values, int left, int right) { +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; @@ -43,54 +46,60 @@ int OlesnitskiyVHoareSortSimpleMergeSEQ::HoarePartition(std::vector &values ## Детали реализации -`ValidationImpl` принимает только непустой вход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L66)). -`PreProcessingImpl` копирует вход в выходной буфер, чтобы сортировать результат без изменения -`GetInput()` ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L70)). `RunImpl` пропускает массивы -размера `0/1`, затем вызывает `HoareQuickSort` и проверяет `std::ranges::is_sorted` -([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L75)). `PostProcessingImpl` повторно проверяет -непустой и отсортированный выход ([`seq/src/ops_seq.cpp`](src/ops_seq.cpp#L86)). +`ValidationImpl` принимает только непустой вход +(`seq/src/ops_seq.cpp:66`). `PreProcessingImpl` копирует +вход в выходной буфер, чтобы сортировать результат без изменения `GetInput()` +(`seq/src/ops_seq.cpp:70`). `RunImpl` пропускает массивы +размера `0/1`, затем вызывает `HoareQuickSort` и проверяет +`std::ranges::is_sorted` (`seq/src/ops_seq.cpp:75`). +`PostProcessingImpl` повторно проверяет непустой и отсортированный выход +(`seq/src/ops_seq.cpp:86`). ## Проверка корректности -Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и сравнивает его -с выходом задачи ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). -Набор содержит 15 сценариев: один элемент, дубликаты, обратный порядок, отрицательные +Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и +сравнивает его с выходом задачи +(`tests/functional/main.cpp:35`). Набор +содержит 15 сценариев: один элемент, дубликаты, обратный порядок, отрицательные значения, границу блока 64 и `int`-пределы -([`tests/functional/main.cpp`](../tests/functional/main.cpp#L59)). Запуск -`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-тестов. +(`tests/functional/main.cpp:59`). Запуск +`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-тестов. ## Экспериментальная среда -Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и oneTBB -видны в [`build_olesnitskiy/compile_commands.json`](../../../build_olesnitskiy/compile_commands.json). +Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и +oneTBB видны в +`build_olesnitskiy/compile_commands.json`. Размер performance-набора: 100000 случайных `int` из `[-1000000, 1000000]` -([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). Время печатает -`Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` -([`modules/util/include/perf_test_util.hpp`](../../../modules/util/include/perf_test_util.hpp#L87)). +(`tests/performance/main.cpp:20`). Время +печатает `Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` +(`modules/util/include/perf_test_util.hpp:87`). Число повторов по умолчанию равно 5 -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). -Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного `PreProcessing()` -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L62)), -а вход для каждого gtest-параметра генерируется заново через `std::random_device` -([`tests/performance/main.cpp`](../tests/performance/main.cpp#L24)). +(`modules/performance/include/performance.hpp:21`). +Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного +`PreProcessing()` +(`modules/performance/include/performance.hpp:62`), +а вход для каждого gtest-параметра генерируется заново через +`std::random_device` +(`tests/performance/main.cpp:24`). ## Результаты -Baseline измерен командой: -`PPC_NUM_THREADS=1 ./build_olesnitskiy/bin/ppc_perf_tests` -с фильтром `*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled`. +Baseline измерен командой: `PPC_NUM_THREADS=1 +./build_olesnitskiy/bin/ppc_perf_tests` с фильтром +`*task_run*olesnitskiy_v_hoare_sort_simple_merge_seq_enabled`. -| backend | time | speedup | efficiency | notes | -| --- | ---: | ---: | ---: | --- | -| seq | 0.0058254364 s | 1.000 | 1.000 | `N=100000`, `TaskRun`, `PPC_NUM_THREADS=1` | -| seq | 0.0068995056 s | 1.000 | 1.000 | `N=100000`, `pipeline`, `PPC_NUM_THREADS=1` | +- 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`. ## Выводы -Последовательная версия является baseline для speedup. Корректность подтверждена сравнением -с `std::ranges::sort`; производительность подтверждена `TaskRun` и `pipeline`-замерами на -`N=100000`. +Последовательная версия является baseline для speedup. Корректность подтверждена +сравнением с `std::ranges::sort`; производительность подтверждена `TaskRun` и +`pipeline`-замерами на `N=100000`. diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md index 9000b0739d..2472a4e153 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -2,28 +2,36 @@ ## Контекст и базовый алгоритм -STL-версия использует `std::thread`: массив делится на блоки по 64 элемента, каждый блок сортируется quicksort Хоара, -после чего уровни простого слияния выполняются параллельно по независимым парам диапазонов -([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L16), [`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L143)). Вход и выход имеют тип -`std::vector` ([`common/include/common.hpp`](../common/include/common.hpp#L11)). +STL-версия использует `std::thread`: массив делится на блоки по 64 элемента, +каждый блок сортируется quicksort Хоара, после чего уровни простого слияния +выполняются параллельно по независимым парам диапазонов +(`stl/src/ops_stl.cpp:16`, +`stl/src/ops_stl.cpp:143`). Вход и выход имеют тип +`std::vector` +(`common/include/common.hpp:11`). ## Разбиение, буферы и синхронизация -Количество потоков выбирается внутри реализации как `min(task_count, hardware_concurrency)`; переменная окружения -`PPC_NUM_THREADS` в STL-коде не читается. Если `hardware_concurrency()` вернул `0`, используется fallback `2` -([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L18)). На тестовой машине `nproc` вернул `12`, поэтому в таблице конфигурация -обозначена как `auto (12)`, а не как вручную заданные 12 потоков. `RunInThreads` распределяет задачи циклически: -`task_index += thread_count` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L41)). Локальные буферы представлены стековыми -переменными lambda, общий `merged_data` безопасен, потому что каждый task пишет только свой диапазон -([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L163)). `atomic` и `mutex` не используются, потому что общей изменяемой -скалярной структуры нет. - -`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь пул рабочих потоков, а затем дождаться -завершения каждого. Если делать `join` сразу после `emplace_back`, выполнение стало бы последовательным по потокам. -Семантика `join` подтверждена cppreference: завершение потока синхронизируется с успешным возвратом из `join` -([cppreference `std::thread::join`](https://en.cppreference.com/w/cpp/thread/thread/join.html)). - -Фрагмент, [`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L38): создание всех потоков и последующий join. +Количество потоков выбирается внутри реализации как `min(task_count, +hardware_concurrency)`; переменная окружения `PPC_NUM_THREADS` в STL-коде не +читается. Если `hardware_concurrency()` вернул `0`, используется fallback `2` +(`stl/src/ops_stl.cpp:18`). На тестовой машине `nproc` +вернул `12`, поэтому в таблице конфигурация обозначена как `auto (12)`, а не как +вручную заданные 12 потоков. `RunInThreads` распределяет задачи циклически: +`task_index += thread_count` (`stl/src/ops_stl.cpp:41`). +Локальные буферы представлены стековыми переменными lambda, общий `merged_data` +безопасен, потому что каждый task пишет только свой диапазон +(`stl/src/ops_stl.cpp:163`). `atomic` и `mutex` не +используются, потому что общей изменяемой скалярной структуры нет. + +`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь +пул рабочих потоков, а затем дождаться завершения каждого. Если делать `join` +сразу после `emplace_back`, выполнение стало бы последовательным по потокам. +Семантика `join` подтверждена cppreference: завершение потока синхронизируется с +успешным возвратом из `join`. + +Фрагмент, `stl/src/ops_stl.cpp:38`: создание всех потоков и +последующий join. ```cpp std::vector threads; @@ -31,7 +39,8 @@ 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) { + for (size_t task_index = thread_index; task_index < task_count; + task_index += thread_count) { function(task_index); } }); @@ -44,33 +53,42 @@ for (auto &thread : threads) { ## Детали pipeline -`ValidationImpl` проверяет непустой вход ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L133)). `PreProcessingImpl` копирует -вход в `data_` ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L137)). `RunImpl` сортирует блоки и сливает уровни -([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L143)). `PostProcessingImpl` проверяет сортировку и записывает результат в -`GetOutput()` только при успехе ([`stl/src/ops_stl.cpp`](src/ops_stl.cpp#L182)). +`ValidationImpl` проверяет непустой вход +(`stl/src/ops_stl.cpp:133`). `PreProcessingImpl` копирует +вход в `data_` (`stl/src/ops_stl.cpp:137`). `RunImpl` +сортирует блоки и сливает уровни +(`stl/src/ops_stl.cpp:143`). `PostProcessingImpl` проверяет +сортировку и записывает результат в `GetOutput()` только при успехе +(`stl/src/ops_stl.cpp:182`). ## Корректность и среда В исходном тесте STL-backend добавлен в общий список задач -([`tests/functional/main.cpp`](../tests/functional/main.cpp#L84)), -а эталон строится через `std::ranges::sort` ([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). Свежий -запуск `build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел под -`mpirun -np 2`: 15 passed. +(`tests/functional/main.cpp:84`), а эталон +строится через `std::ranges::sort` +(`tests/functional/main.cpp:35`). Свежий запуск +`build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed; +ALL отдельно прошел под `mpirun -np 2`: 15 passed. ## Результаты -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Performance-вход описан в -[`tests/performance/main.cpp`](../tests/performance/main.cpp#L20). Framework выполняет 5 повторов по умолчанию -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 +s`. Performance-вход описан в +`tests/performance/main.cpp:20`. Framework +выполняет 5 повторов по умолчанию +(`modules/performance/include/performance.hpp:21`). -| workers | time | speedup | efficiency | notes | -| --- | ---: | ---: | ---: | --- | -| auto (12 на тестовой машине) | 0.0047718214 s | 1.221 | 0.102 | `TaskRun`; `hardware_concurrency()` | -| auto (12 на тестовой машине) | 0.0055675704 s | 1.239 | 0.103 | `pipeline`; env=1 не влияет | -| auto (12 на тестовой машине) | 0.0081357990 s | 0.848 | 0.071 | `pipeline`; env=4 не влияет | +- 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 не влияет. ## Выводы -По коду STL-версия имеет те же фазы, что TBB: независимая сортировка блоков и независимое слияние. На `N=100000` -получено `0.0047718214 s`, speedup `1.221`; efficiency низкая (`0.102`), потому что реализация автоматически создает -до `hardware_concurrency()` рабочих потоков, а объем работы мал для такого числа workers. +По коду 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/tbb/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md index 88da9cd053..96e20dbedf 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md @@ -2,30 +2,40 @@ ## Контекст и базовый алгоритм -TBB-версия сортирует массив `int` блоками по 64 элемента, затем сливает соседние отсортированные диапазоны -([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L18), [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L127)). Локальная сортировка -использует ту же схему Хоара, что и `seq`: pivot из середины, два индекса и обмен до пересечения -([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L28)). +TBB-версия сортирует массив `int` блоками по 64 элемента, затем сливает соседние +отсортированные диапазоны (`tbb/src/ops_tbb.cpp:18`, +`tbb/src/ops_tbb.cpp:127`). Локальная сортировка использует +ту же схему Хоара, что и `seq`: pivot из середины, два индекса и обмен до +пересечения (`tbb/src/ops_tbb.cpp:28`). ## TBB-примитивы -Используется `oneapi::tbb::parallel_for` с `oneapi::tbb::blocked_range` для диапазона индексов блоков и индексов -слияния ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L117), [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L131)). Grainsize явно -не передан, поэтому применяется значение конструктора `blocked_range` по умолчанию; partitioner также явно не указан, -значит используется стандартное разбиение `parallel_for`. Конкуренция ограничивается раннером через +Используется `oneapi::tbb::parallel_for` с `oneapi::tbb::blocked_range` +для диапазона индексов блоков и индексов слияния +(`tbb/src/ops_tbb.cpp:117`, +`tbb/src/ops_tbb.cpp:131`). Grainsize явно не передан, +поэтому применяется значение конструктора `blocked_range` по умолчанию; +partitioner также явно не указан, значит используется стандартное разбиение +`parallel_for`. Конкуренция ограничивается раннером через `tbb::global_control(max_allowed_parallelism, ppc::util::GetNumThreads())` -([`modules/runners/src/runners.cpp`](../../../modules/runners/src/runners.cpp#L150)); `GetNumThreads` читает -`PPC_NUM_THREADS` ([`modules/util/src/util.cpp`](../../../modules/util/src/util.cpp#L23)). +(`modules/runners/src/runners.cpp:150`); +`GetNumThreads` читает `PPC_NUM_THREADS` +(`modules/util/src/util.cpp:23`). -Фрагмент, [`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L117): `blocked_range` задает независимые номера блоков. +Фрагмент, `tbb/src/ops_tbb.cpp:117`: `blocked_range` задает +независимые номера блоков. ```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) { +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)); + HoareQuickSort(data_, static_cast(block_start), + static_cast(block_end - 1)); } } }); @@ -34,41 +44,48 @@ 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) { + oneapi::tbb::parallel_for( + oneapi::tbb::blocked_range(0, merge_count), + [this, size, merge_width, &merged_data](const auto &range) { ``` ## Детали pipeline и гонки -`ValidationImpl`, `PreProcessingImpl`, `RunImpl`, `PostProcessingImpl` расположены в -[`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L99). Сортировка блоков пишет в непересекающиеся отрезки `data_`; слияние пишет -в непересекающиеся отрезки `merged_data`, читая `data_` ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L138)). `data_.swap` -выполняется после завершения `parallel_for` ([`tbb/src/ops_tbb.cpp`](src/ops_tbb.cpp#L148)). +`ValidationImpl`, `PreProcessingImpl`, `RunImpl`, `PostProcessingImpl` +расположены в `tbb/src/ops_tbb.cpp:99`. Сортировка блоков +пишет в непересекающиеся отрезки `data_`; слияние пишет в непересекающиеся +отрезки `merged_data`, читая `data_` +(`tbb/src/ops_tbb.cpp:138`). `data_.swap` выполняется после +завершения `parallel_for` (`tbb/src/ops_tbb.cpp:148`). ## Корректность и среда Функциональный тест сравнивает результат с `std::ranges::sort` -([`tests/functional/main.cpp`](../tests/functional/main.cpp#L35)). -Запуск текущего `build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 passed; ALL отдельно прошел -под `mpirun -np 2`: 15 passed. Performance-вход: `N=100000` -([`tests/performance/main.cpp`](../tests/performance/main.cpp#L20)). +(`tests/functional/main.cpp:35`). Запуск +текущего `build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 +passed; ALL отдельно прошел под `mpirun -np 2`: 15 passed. Performance-вход: +`N=100000` (`tests/performance/main.cpp:20`). ## Результаты -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 s`. Framework выполняет 5 повторов -по умолчанию -([`modules/performance/include/performance.hpp`](../../../modules/performance/include/performance.hpp#L21)). - -| threads | time | speedup | efficiency | notes | -| ---: | ---: | ---: | ---: | --- | -| 1 | 0.0024417256 s | 2.386 | 2.386 | `TaskRun`, `PPC_NUM_THREADS=1` | -| 2 | 0.0014976288 s | 3.889 | 1.945 | `TaskRun`, `PPC_NUM_THREADS=2` | -| 4 | 0.0011774682 s | 4.947 | 1.237 | `TaskRun`, `PPC_NUM_THREADS=4` | -| 1 | 0.0067520042 s | 1.022 | 1.022 | `pipeline`, `PPC_NUM_THREADS=1` | -| 4 | 0.0026312252 s | 2.622 | 0.656 | `pipeline`, `PPC_NUM_THREADS=4` | +Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 +s`. Framework выполняет 5 повторов по умолчанию +(`modules/performance/include/performance.hpp:21`). + +- 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`. ## Выводы -В измерениях TBB показал лучший результат среди потоковых backend-ов: `0.0011774682 s` -при 4 потоках, speedup `4.947`. Числа относятся к `N=100000` и фиксируются командой `ppc_perf_tests`; вывод не -распространяется на другие размеры без дополнительных замеров. +В измерениях TBB показал лучший результат среди потоковых backend-ов: +`0.0011774682 s` при 4 потоках, speedup `4.947`. Числа относятся к `N=100000` и +фиксируются командой `ppc_perf_tests`; вывод не распространяется на другие +размеры без дополнительных замеров. From eebc47390a77b016095ebc92ec2dd4a07cbc8341 Mon Sep 17 00:00:00 2001 From: milka Date: Wed, 20 May 2026 08:50:20 +0000 Subject: [PATCH 8/8] o my god --- .../all/report.md | 126 +++++++------ .../omp/report.md | 128 ++++++------- .../report.md | 169 ++++++++++-------- .../seq/report.md | 115 ++++++------ .../stl/report.md | 121 +++++++------ .../tbb/report.md | 97 +++++----- 6 files changed, 413 insertions(+), 343 deletions(-) diff --git a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md index 7021f39756..95171937f9 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/all/report.md @@ -1,27 +1,36 @@ -# Отчет ALL: MPI + STL для сортировки Хоара с простым слиянием +# Сортировка Хоара с простым слиянием — ALL -## Контекст и базовый алгоритм +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** ALL (MPI + STL) +- **Вариант:** 13 -ALL-версия объединяет MPI между процессами и `std::thread` внутри процесса. -Корневой rank хранит входной массив, данные распределяются между rank-ами, +## 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 и последовательно досливаются -(`all/src/ops_all.cpp:227`). +собираются на rank 0 и последовательно досливаются. -## Межпроцессная схема +## 4. Межпроцессная схема -`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов -(`all/src/ops_all.cpp:228`). `BuildDistribution` делит -`total_size` почти поровну: первые `remainder` rank-ов получают на один элемент -больше (`all/src/ops_all.cpp:64`). `MPI_Scatterv` отправляет -локальные куски, `MPI_Gatherv` собирает отсортированные куски на rank 0, затем -`BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast` -(`all/src/ops_all.cpp:246`, -`all/src/ops_all.cpp:256`, -`all/src/ops_all.cpp:264`). +`MPI_Comm_rank` и `MPI_Comm_size` определяют роль процесса и число rank-ов. +`BuildDistribution` делит `total_size` почти поровну: первые `remainder` +rank-ы получают на один элемент больше. `MPI_Scatterv` отправляет локальные +куски, `MPI_Gatherv` собирает отсортированные куски на rank 0, затем +`BroadcastVector` рассылает итог всем rank-ам через `MPI_Bcast`. -Фрагмент, `all/src/ops_all.cpp:238`: распределение, -scatter/gather и broadcast. +Фрагмент распределения и обмена: ```cpp std::vector chunk_sizes(static_cast(mpi_size)); @@ -44,41 +53,43 @@ if (mpi_rank == 0) { } ``` -## Внутрипроцессная схема +## 5. Внутрипроцессная схема Локальная сортировка повторяет STL-версию: `RunInThreads`, блоки по 64, затем -уровни слияния (`all/src/ops_all.cpp:167`). Количество -потоков внутри rank-а выбирается по `std::thread::hardware_concurrency()` и -числу локальных задач (`all/src/ops_all.cpp:20`). - -## Конфигурация ranks × threads и цена синхронизации - -Для ALL нужно указывать `ranks`, `threads_per_rank`, `total_workers = ranks * -threads_per_rank`. В коде явного `MPI_Barrier` нет; обязательная цена -синхронизации возникает на коллективных операциях `MPI_Scatterv`, `MPI_Gatherv` -и `MPI_Bcast`. Дополнительный `MPI_Barrier` добавляет тестовый listener раннера -после каждого теста -(`modules/runners/src/runners.cpp:23`). - -## Детали pipeline и корректность - -`ValidationImpl` проверяет непустой вход -(`all/src/ops_all.cpp:217`). `PreProcessingImpl` копирует -вход в `data_` (`all/src/ops_all.cpp:221`). `RunImpl` -выполняет MPI-распределение, локальную сортировку, сбор и broadcast -(`all/src/ops_all.cpp:227`). `PostProcessingImpl` проверяет -`std::ranges::is_sorted` и записывает выход -(`all/src/ops_all.cpp:268`). В исходном функциональном тесте -ALL-backend зарегистрирован -(`tests/functional/main.cpp:88`). - -## Результаты - -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 -s`. Performance-вход `N=100000` -(`tests/performance/main.cpp:20`). Framework -выполняет 5 повторов по умолчанию -(`modules/performance/include/performance.hpp:21`). +уровни слияния. Количество потоков внутри 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`, @@ -90,13 +101,12 @@ s`. Performance-вход `N=100000` 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. + 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 (`all/src/ops_all.cpp:204`). На 4 rank-ах получено -`0.0043261284 s`, speedup `1.347`; efficiency `0.028`, потому что каждый rank -дополнительно запускает до 12 STL-потоков, а коммуникации остаются -обязательными. +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/omp/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md index 07aa54a371..aabc642b0f 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/omp/report.md @@ -1,31 +1,43 @@ -# Отчет OMP: сортировка Хоара с простым слиянием - -## Контекст и базовый алгоритм - -Вход и выход: `std::vector` -(`common/include/common.hpp:11`). -Последовательное ядро: разбиение Хоара и quicksort по локальным диапазонам -(`omp/src/ops_omp.cpp:21`). После локальной сортировки -блоков по 64 элемента выполняется попарное простое слияние до полного массива -(`omp/src/ops_omp.cpp:115`). Средняя сложность локальной -quicksort-части `O(n log n)`, слияния по уровням дают дополнительный линейный -проход на каждом уровне. - -## Схема распараллеливания - -OpenMP используется в двух `parallel for`: сортировка независимых блоков и -слияние независимых пар блоков (`omp/src/ops_omp.cpp:119`, -`omp/src/ops_omp.cpp:130`). В директивах стоит -`default(none)`, поэтому все shared-переменные перечислены явно: `size`, `data`, -`merged_data`, `merge_width`. Индексы циклов и временные векторы являются -private по области видимости тела цикла. Reduction не нужен: нет общей скалярной -агрегации. Явный `schedule` не задан, значит применяется runtime-default OpenMP; -для равномерных блоков по 64 элемента это соответствует идее статического -распределения без дополнительных вычислений планировщика. Барьер OpenMP в конце -каждого `parallel for` нужен перед `data.swap(merged_data)`. - -Фрагмент, `omp/src/ops_omp.cpp:119`: параллельные блоки -пишут в непересекающиеся диапазоны. +# Сортировка Хоара с простым слиянием — 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) @@ -49,42 +61,39 @@ private по области видимости тела цикла. Reduction н const std::size_t right = std::min(left + (2 * merge_width), size); ``` -## Детали pipeline - -`ValidationImpl` проверяет непустой вход -(`omp/src/ops_omp.cpp:100`). `PreProcessingImpl` копирует -вход в `data_` и очищает выход (`omp/src/ops_omp.cpp:104`). -`RunImpl` сортирует блоки и выполняет итеративное слияние -(`omp/src/ops_omp.cpp:110`). `PostProcessingImpl` проверяет -`data_` через `std::ranges::is_sorted` и только после этого записывает -`GetOutput()` (`omp/src/ops_omp.cpp:156`). +## 5. Детали реализации -## Проверка отсутствия гонок +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в +`data_` и очищает выход. `RunImpl` сортирует блоки и выполняет итеративное +слияние. `PostProcessingImpl` проверяет `data_` через +`std::ranges::is_sorted` и только после этого записывает `GetOutput()`. При сортировке блок `block_start..block_end` не пересекается с соседним, потому -что шаг равен `kBlockSize` (`omp/src/ops_omp.cpp:120`). При -слиянии каждый поток пишет в `merged_data[left, right)`, где `left` идет с шагом -`2 * merge_width` (`omp/src/ops_omp.cpp:131`). Общий `data` -на фазе слияния только читается, а `data.swap(merged_data)` выполняется после -завершения параллельного цикла (`omp/src/ops_omp.cpp:150`). +что шаг равен `kBlockSize`. При слиянии каждый поток пишет в свой диапазон +`merged_data[left, right)`. Общий `data` на фазе слияния только читается, а +`data.swap(merged_data)` выполняется после завершения параллельного цикла. -## Корректность и среда +## 6. Проверка корректности -Эталон в функциональных тестах строится `std::ranges::sort` -(`tests/functional/main.cpp:35`). Запуск +Эталон в функциональных тестах строится через `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. Performance-вход: 100000 случайных чисел -(`tests/performance/main.cpp:20`). +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`. -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 -s`. Потоки задавались через `PPC_NUM_THREADS`; раннер ограничивает TBB этой же -переменной, а OpenMP использует ее через окружение сборки/запуска. Framework -выполняет 5 повторов по умолчанию -(`modules/performance/include/performance.hpp:21`). +## 8. Результаты - threads: 1; time: 0.2042550788 s; speedup: 0.029; efficiency: 0.029; notes: `TaskRun`, `N=100000`. @@ -97,10 +106,9 @@ s`. Потоки задавались через `PPC_NUM_THREADS`; раннер - threads: 4; time: 0.1193738506 s; speedup: 0.058; efficiency: 0.014; notes: `pipeline`, `N=100000`. -## Выводы +## 9. Выводы -На данном размере входа OpenMP-версия медленнее baseline: лучший замер -`0.1880041056 s` при 4 потоках дает speedup `0.031`. Причина подтверждается -кодом: блок `kBlockSize=64` создает много мелких задач и временных векторов при -слиянии (`omp/src/ops_omp.cpp:115`, -`omp/src/ops_omp.cpp:136`). +На данном размере входа 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 index 7680e67881..06e491bf5f 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/report.md @@ -1,52 +1,84 @@ -# Сводный отчет: сортировка Хоара с простым слиянием +# Сортировка Хоара с простым слиянием — сводный отчет -## Введение и постановка +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технологии:** SEQ, OMP, TBB, STL, ALL (MPI + STL) +- **Вариант:** 13 + +## 1. Контекст Работа сравнивает реализации сортировки `std::vector` по неубыванию для -backend-ов `seq`, `omp`, `tbb`, `stl` и `all`. Единые типы входа/выхода заданы в -`common/include/common.hpp:11`. Корректность -определяется совпадением с результатом `std::ranges::sort` в функциональном -тесте (`tests/functional/main.cpp:35`). - -## Единая методика эксперимента - -Performance-тест генерирует `N=100000` случайных `int` из диапазона `[-1000000, -1000000]` (`tests/performance/main.cpp:20`). -Использован `TaskRun`; режим выбирается в раннере -(`modules/util/include/perf_test_util.hpp:87`). -Speedup считался как `T_seq / T_backend`, efficiency как `speedup / workers`. -Baseline: `seq`, `PPC_NUM_THREADS=1`, `T_seq = 0.0058254364 s`. Для OMP/TBB -применялись `PPC_NUM_THREADS=1,2,4`; TBB дополнительно ограничивается -`tbb::global_control` -(`modules/runners/src/runners.cpp:150`). -STL и локальная часть ALL берут число потоков из -`std::thread::hardware_concurrency()`; на этой машине `nproc = 12`, поэтому -такие строки обозначены как auto workers. Число повторов по умолчанию равно 5 -(`modules/performance/include/performance.hpp:21`). +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()` -(`modules/performance/include/performance.hpp:62`), -а вход создается заново для каждого gtest-параметра через `std::random_device` -(`tests/performance/main.cpp:24`). Поэтому -дополнительно сняты pipeline-замеры, где каждый повтор проходит полный pipeline. - -Среда: свежая Release-сборка `build_olesnitskiy`, `g++-14`, `-O3 -DNDEBUG`, -`std=gnu++23`; эти флаги видны в -`build_olesnitskiy/compile_commands.json`. -Число повторов задается framework-ом `Perf::TaskRun`; отчет опирается на -напечатанные строки `backend:task_run:time`. - -## Сводка корректности - -Источник функциональных тестов регистрирует все пять backend-ов -(`tests/functional/main.cpp:80`). Запуск -`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests ---gtest_filter='*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-тестов. - -## Агрегированные результаты +`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. @@ -71,7 +103,7 @@ passed для `seq/omp/stl/tbb`, 15 ALL skipped вне MPI. Отдельный - backend: all, 4 ranks x 12 threads; time: 0.0043261284 s; speedup: 1.347; efficiency: 0.028; notes: total_workers=48. -Дополнительные pipeline-замеры: +## 9. Результаты PipelineRun - backend: seq; time: 0.0068995056 s; speedup: 1.000; efficiency: 1.000; notes: `pipeline`, baseline. @@ -88,22 +120,20 @@ passed для `seq/omp/stl/tbb`, 15 ALL skipped вне MPI. Отдельный - 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`. -`seq` сортирует весь массив одним quicksort и служит baseline -(`seq/src/ops_seq.cpp:75`). `omp` создает параллельные -области для множества блоков по 64 и выделяет временные векторы при слиянии -(`omp/src/ops_omp.cpp:115`, -`omp/src/ops_omp.cpp:136`); на `N=100000` измеренный -speedup меньше 1. `tbb` использует `parallel_for(blocked_range)` для блоков и -слияний (`tbb/src/ops_tbb.cpp:117`); при 4 workers -получено `4.947x`. `stl` вручную создает и join-ит потоки -(`stl/src/ops_stl.cpp:38`); при 12 workers получено -`1.221x`, но efficiency `0.102`. `all` добавляет `MPI_Scatterv/Gatherv/Bcast` и -финальное слияние на rank 0 (`all/src/ops_all.cpp:246`); -на 4 rank-ах время `0.0043261284 s`, но efficiency `0.028`. +`all` добавляет `MPI_Scatterv/Gatherv/Bcast` и финальное слияние на rank 0. На +4 rank-ах время `0.0043261284 s`, но efficiency равна `0.028`. -## Репродуцируемость +## 11. Репродуцируемость Сборка: @@ -136,21 +166,10 @@ 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 из-за накладных -расходов. - -## Источники - -1. OpenMP API Specification 5.2, разделы data-sharing и schedule: - https://www.openmp.org/spec-html/5.2/openmp.html -2. oneTBB `parallel_for` and `blocked_range`: - https://uxlfoundation.github.io/oneTBB/ -3. MPI Standard, коллективные операции: - https://www.mpi-forum.org/docs/ -4. `std::thread::join`: - https://en.cppreference.com/w/cpp/thread/thread/join.html +(`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 index cf71efdffa..e3d7c05a66 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/seq/report.md @@ -1,21 +1,37 @@ -# Отчет SEQ: сортировка Хоара с простым слиянием +# Сортировка Хоара с простым слиянием — SEQ -## Контекст и постановка +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** SEQ +- **Вариант:** 13 -Задача: отсортировать `std::vector` по неубыванию. Типы входа и выхода -заданы как `std::vector` в -`common/include/common.hpp:11`. -Последовательная версия использует разбиение Хоара и итеративный quicksort: -опорный элемент берется из середины диапазона, индексы двигаются навстречу, -элементы меняются местами до пересечения -(`seq/src/ops_seq.cpp:18`). Средняя сложность сортировки: -`O(n log n)`, худшая: `O(n^2)`, память стека диапазонов: до `O(n)` в -неблагоприятном случае. Инвариант разбиения: после возврата `j` элементы слева -от границы не больше опорной группы, справа не меньше; рекурсивные подзадачи -заменены стеком диапазонов (`seq/src/ops_seq.cpp:42`). +## 1. Контекст -Фрагмент реализации, `seq/src/ops_seq.cpp:18`: выбор pivot и -схема Хоара. +Последовательная версия является базовым эталоном для всех остальных реализаций +задачи. Она сортирует `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( @@ -44,50 +60,44 @@ int OlesnitskiyVHoareSortSimpleMergeSEQ::HoarePartition( } ``` -## Детали реализации +## 4. Детали реализации + +`ValidationImpl` принимает только непустой вход. `PreProcessingImpl` копирует +вход в выходной буфер, чтобы сортировать результат без изменения исходного +контейнера. `RunImpl` пропускает массивы размера `0/1`, затем вызывает +`HoareQuickSort` и проверяет `std::ranges::is_sorted`. -`ValidationImpl` принимает только непустой вход -(`seq/src/ops_seq.cpp:66`). `PreProcessingImpl` копирует -вход в выходной буфер, чтобы сортировать результат без изменения `GetInput()` -(`seq/src/ops_seq.cpp:70`). `RunImpl` пропускает массивы -размера `0/1`, затем вызывает `HoareQuickSort` и проверяет -`std::ranges::is_sorted` (`seq/src/ops_seq.cpp:75`). -`PostProcessingImpl` повторно проверяет непустой и отсортированный выход -(`seq/src/ops_seq.cpp:86`). +`PostProcessingImpl` повторно проверяет непустой и отсортированный выход. Это +фиксирует базовый pipeline задачи: валидация, подготовка, сортировка и проверка +результата. -## Проверка корректности +## 5. Проверка корректности Функциональный тест строит эталон через `std::ranges::sort(expected_data)` и -сравнивает его с выходом задачи -(`tests/functional/main.cpp:35`). Набор -содержит 15 сценариев: один элемент, дубликаты, обратный порядок, отрицательные -значения, границу блока 64 и `int`-пределы -(`tests/functional/main.cpp:59`). Запуск -`PPC_NUM_THREADS=4 ./build_olesnitskiy/bin/ppc_func_tests` с фильтром +сравнивает его с выходом задачи. Набор содержит 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-тестов. -## Экспериментальная среда - -Сборка `build_olesnitskiy`: `g++-14`, `-O3 -DNDEBUG`, `std=gnu++23`, OpenMPI и -oneTBB видны в -`build_olesnitskiy/compile_commands.json`. -Размер performance-набора: 100000 случайных `int` из `[-1000000, 1000000]` -(`tests/performance/main.cpp:20`). Время -печатает `Perf::PrintPerfStatistic` после `TaskRun` или `PipelineRun` -(`modules/util/include/perf_test_util.hpp:87`). -Число повторов по умолчанию равно 5 -(`modules/performance/include/performance.hpp:21`). +## 6. Экспериментальная среда + +- **Сборка:** `build_olesnitskiy` +- **Compiler:** `g++-14` +- **Flags:** `-O3 -DNDEBUG`, `std=gnu++23` +- **Размер входных данных:** `N=100000` +- **Диапазон значений:** `[-1000000, 1000000]` +- **Число повторов:** 5 по умолчанию в `Perf::TaskRun` + Важно для интерпретации: `TaskRun` повторяет только `Run()` после одного -`PreProcessing()` -(`modules/performance/include/performance.hpp:62`), -а вход для каждого gtest-параметра генерируется заново через -`std::random_device` -(`tests/performance/main.cpp:24`). +`PreProcessing()`, а вход для каждого gtest-параметра генерируется заново через +`std::random_device`. -## Результаты +## 7. Результаты Baseline измерен командой: `PPC_NUM_THREADS=1 ./build_olesnitskiy/bin/ppc_perf_tests` с фильтром @@ -98,8 +108,9 @@ Baseline измерен командой: `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. Выводы -Последовательная версия является baseline для speedup. Корректность подтверждена -сравнением с `std::ranges::sort`; производительность подтверждена `TaskRun` и -`pipeline`-замерами на `N=100000`. +Последовательная версия подтверждает корректность алгоритма сравнением с +`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/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md index 2472a4e153..bf80c9c329 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/stl/report.md @@ -1,37 +1,42 @@ -# Отчет STL: сортировка Хоара с простым слиянием - -## Контекст и базовый алгоритм - -STL-версия использует `std::thread`: массив делится на блоки по 64 элемента, -каждый блок сортируется quicksort Хоара, после чего уровни простого слияния -выполняются параллельно по независимым парам диапазонов -(`stl/src/ops_stl.cpp:16`, -`stl/src/ops_stl.cpp:143`). Вход и выход имеют тип -`std::vector` -(`common/include/common.hpp:11`). - -## Разбиение, буферы и синхронизация - -Количество потоков выбирается внутри реализации как `min(task_count, -hardware_concurrency)`; переменная окружения `PPC_NUM_THREADS` в STL-коде не -читается. Если `hardware_concurrency()` вернул `0`, используется fallback `2` -(`stl/src/ops_stl.cpp:18`). На тестовой машине `nproc` -вернул `12`, поэтому в таблице конфигурация обозначена как `auto (12)`, а не как -вручную заданные 12 потоков. `RunInThreads` распределяет задачи циклически: -`task_index += thread_count` (`stl/src/ops_stl.cpp:41`). -Локальные буферы представлены стековыми переменными lambda, общий `merged_data` -безопасен, потому что каждый task пишет только свой диапазон -(`stl/src/ops_stl.cpp:163`). `atomic` и `mutex` не -используются, потому что общей изменяемой скалярной структуры нет. +# Сортировка Хоара с простым слиянием — STL -`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь -пул рабочих потоков, а затем дождаться завершения каждого. Если делать `join` -сразу после `emplace_back`, выполнение стало бы последовательным по потокам. -Семантика `join` подтверждена cppreference: завершение потока синхронизируется с -успешным возвратом из `join`. +- **Студент:** Олесницкий Владимир Тарасович, 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 Хоара, +после чего уровни простого слияния выполняются параллельно по независимым парам +диапазонов. -Фрагмент, `stl/src/ops_stl.cpp:38`: создание всех потоков и -последующий join. +## 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; @@ -51,32 +56,36 @@ for (auto &thread : threads) { } ``` -## Детали pipeline +`join` вызывается после запуска всех `std::thread`, чтобы сначала создать весь +пул рабочих потоков, а затем дождаться завершения каждого. Если делать `join` +сразу после `emplace_back`, выполнение стало бы последовательным по потокам. + +## 5. Детали реализации -`ValidationImpl` проверяет непустой вход -(`stl/src/ops_stl.cpp:133`). `PreProcessingImpl` копирует -вход в `data_` (`stl/src/ops_stl.cpp:137`). `RunImpl` -сортирует блоки и сливает уровни -(`stl/src/ops_stl.cpp:143`). `PostProcessingImpl` проверяет -сортировку и записывает результат в `GetOutput()` только при успехе -(`stl/src/ops_stl.cpp:182`). +`ValidationImpl` проверяет непустой вход. `PreProcessingImpl` копирует вход в +`data_`. `RunImpl` сортирует блоки и сливает уровни. `PostProcessingImpl` +проверяет сортировку и записывает результат в `GetOutput()` только при успехе. -## Корректность и среда +## 6. Проверка корректности -В исходном тесте STL-backend добавлен в общий список задач -(`tests/functional/main.cpp:84`), а эталон -строится через `std::ranges::sort` -(`tests/functional/main.cpp:35`). Свежий запуск -`build_olesnitskiy/bin/ppc_func_tests` подтвердил `seq/omp/stl/tbb`: 60 passed; +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-коде не читается. -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 -s`. Performance-вход описан в -`tests/performance/main.cpp:20`. Framework -выполняет 5 повторов по умолчанию -(`modules/performance/include/performance.hpp:21`). +## 8. Результаты - workers: auto (12 на тестовой машине); time: 0.0047718214 s; speedup: 1.221; efficiency: 0.102; notes: `TaskRun`; `hardware_concurrency()`. @@ -85,10 +94,10 @@ s`. Performance-вход описан в - 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`), потому что реализация автоматически создает до +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/tbb/report.md b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md index 96e20dbedf..b2f870697a 100644 --- a/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md +++ b/tasks/olesnitskiy_v_hoare_sort_simple_merge/tbb/report.md @@ -1,29 +1,40 @@ -# Отчет TBB: сортировка Хоара с простым слиянием +# Сортировка Хоара с простым слиянием — TBB -## Контекст и базовый алгоритм +- **Студент:** Олесницкий Владимир Тарасович, 3823Б1ПР2 +- **Технология:** TBB +- **Вариант:** 13 -TBB-версия сортирует массив `int` блоками по 64 элемента, затем сливает соседние -отсортированные диапазоны (`tbb/src/ops_tbb.cpp:18`, -`tbb/src/ops_tbb.cpp:127`). Локальная сортировка использует -ту же схему Хоара, что и `seq`: pivot из середины, два индекса и обмен до -пересечения (`tbb/src/ops_tbb.cpp:28`). +## 1. Контекст -## TBB-примитивы +TBB-версия переносит две независимые фазы алгоритма на `oneapi::tbb`: +сортировку блоков и слияние соседних отсортированных диапазонов. Эта реализация +проверяет, насколько эффективно планировщик TBB распределяет работу для входа +`N=100000`. -Используется `oneapi::tbb::parallel_for` с `oneapi::tbb::blocked_range` -для диапазона индексов блоков и индексов слияния -(`tbb/src/ops_tbb.cpp:117`, -`tbb/src/ops_tbb.cpp:131`). Grainsize явно не передан, -поэтому применяется значение конструктора `blocked_range` по умолчанию; -partitioner также явно не указан, значит используется стандартное разбиение -`parallel_for`. Конкуренция ограничивается раннером через -`tbb::global_control(max_allowed_parallelism, ppc::util::GetNumThreads())` -(`modules/runners/src/runners.cpp:150`); -`GetNumThreads` читает `PPC_NUM_THREADS` -(`modules/util/src/util.cpp:23`). +## 2. Постановка задачи -Фрагмент, `tbb/src/ops_tbb.cpp:117`: `blocked_range` задает -независимые номера блоков. +- **Входные данные:** непустой объект `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( @@ -49,28 +60,31 @@ for (size_t merge_width = kBlockSize; merge_width < size; merge_width *= 2) { [this, size, merge_width, &merged_data](const auto &range) { ``` -## Детали pipeline и гонки +## 5. Детали реализации + +`ValidationImpl`, `PreProcessingImpl`, `RunImpl` и `PostProcessingImpl` +расположены в `tbb/src/ops_tbb.cpp`. Сортировка блоков пишет в +непересекающиеся отрезки `data_`. Слияние пишет в непересекающиеся отрезки +`merged_data`, читая `data_`. `data_.swap` выполняется после завершения +`parallel_for`. -`ValidationImpl`, `PreProcessingImpl`, `RunImpl`, `PostProcessingImpl` -расположены в `tbb/src/ops_tbb.cpp:99`. Сортировка блоков -пишет в непересекающиеся отрезки `data_`; слияние пишет в непересекающиеся -отрезки `merged_data`, читая `data_` -(`tbb/src/ops_tbb.cpp:138`). `data_.swap` выполняется после -завершения `parallel_for` (`tbb/src/ops_tbb.cpp:148`). +## 6. Проверка корректности -## Корректность и среда +Функциональный тест сравнивает результат с `std::ranges::sort`. Запуск текущего +`build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 passed. +ALL отдельно прошел под `mpirun -np 2`: 15 passed. -Функциональный тест сравнивает результат с `std::ranges::sort` -(`tests/functional/main.cpp:35`). Запуск -текущего `build_olesnitskiy/bin/ppc_func_tests` прошел для `seq/omp/stl/tbb`: 60 -passed; ALL отдельно прошел под `mpirun -np 2`: 15 passed. Performance-вход: -`N=100000` (`tests/performance/main.cpp:20`). +## 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 по умолчанию -Baseline: `seq` `TaskRun = 0.0058254364 s`; для pipeline baseline `0.0068995056 -s`. Framework выполняет 5 повторов по умолчанию -(`modules/performance/include/performance.hpp:21`). +## 8. Результаты - threads: 1; time: 0.0024417256 s; speedup: 2.386; efficiency: 2.386; notes: `TaskRun`, `PPC_NUM_THREADS=1`. @@ -83,9 +97,8 @@ s`. Framework выполняет 5 повторов по умолчанию - 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` и -фиксируются командой `ppc_perf_tests`; вывод не распространяется на другие -размеры без дополнительных замеров. +`0.0011774682 s` при 4 потоках, speedup `4.947`. Числа относятся к +`N=100000`; для других размеров нужны отдельные замеры.