diff --git a/tasks/titaev_m_sortirovka_betchera/all/include/ops_all.hpp b/tasks/titaev_m_sortirovka_betchera/all/include/ops_all.hpp new file mode 100644 index 0000000000..af804f58b8 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/all/include/ops_all.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + +#include "task/include/task.hpp" +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +class TitaevSortirovkaBetcheraALL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kALL; + } + explicit TitaevSortirovkaBetcheraALL(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static void ConvertToKeys(const InType &input, std::vector &keys); + static void RadixSort(std::vector &keys); + static void ConvertFromKeys(const std::vector &keys, OutType &output); + void BatcherSort(); + static void BatcherStage(OutType &result, std::size_t array_size, std::size_t block, std::size_t step); +}; + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/all/report.md b/tasks/titaev_m_sortirovka_betchera/all/report.md new file mode 100644 index 0000000000..1117a92c2f --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/all/report.md @@ -0,0 +1,142 @@ +# Поразрядная сортировка для вещественных чисел с чётно-нечётным слиянием Бэтчера - ALL + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: ALL +- Variant: 20 + +## 1. Introduction + +ALL-версия объединяет несколько технологий параллелизма. Цель этой реализации — использовать +MPI-окружение для запуска задачи под несколькими процессами, а внутри каждого процесса дополнительно +применять многопоточность OpenMP на этапах поразрядной сортировки и сети чётно-нечётного слияния Бэтчера. + +## 2. Problem Statement + +Задача совпадает с остальными версиями: отсортировать массив `InType = std::vector` по +неубыванию, результат имеет тип `OutType = std::vector`. + +## 3. Baseline Algorithm (Sequential) + +Baseline описан в `seq/report.md`. Он выполняет преобразование `double` в упорядоченные 64-битные ключи, +поразрядную сортировку по основанию 256 и обратное преобразование, после чего при размере, равном степени +двойки, применяет сеть Бэтчера. + +## 4. Parallelization Scheme + +ALL-реализация использует гибридную схему: + +1. **MPI:** задача исполняется под управлением `mpiexec` с заданным числом процессов; общее число +рабочих определяется как `ranks × threads`. +2. **OpenMP:** внутри процесса распараллеливаются преобразование ключей, подсчёт корзин с локальными +гистограммами и проходы сети Бэтчера через `#pragma omp parallel for`. +3. **Согласование результата:** итоговый отсортированный массив формируется на основе общих входных +данных, доступных каждому процессу. + +Конфигурация задаётся как `workers = ranks × threads`. Например, при запуске `mpiexec -n 4` и +`PPC_NUM_THREADS=2` конфигурация равна `4 × 2`, а `workers = 8`. + +## 5. Implementation Details + +- Файлы: `all/include/ops_all.hpp`, `all/src/ops_all.cpp`. +- Класс: `TitaevSortirovkaBetcheraALL`. +- Этапы преобразования и сети Бэтчера выражены через OpenMP-параллелизм. +- Подсчёт корзин использует локальные гистограммы по потокам для исключения гонок. + +Память: один временный массив ключей и по одной гистограмме на каждый поток. + +Corner cases: + +- При размере массива, не являющемся степенью двойки, сеть Бэтчера не применяется, а итоговый порядок +обеспечивается поразрядной сортировкой. +- Для массива из одного элемента сортировка не требуется. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на упорядоченность по неубыванию. Результаты ALL-версии совпадают с baseline на всех тестовых +наборах. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | +| all | 2 × 1 | 0.079706 | 2.01 | 100.33% | +| all | 2 × 2 | 0.099306 | 1.61 | 40.27% | +| all | 4 × 2 | 0.227350 | 0.70 | 8.79% | + +Наилучший результат получен в конфигурации `2 × 1`. С ростом числа процессов и потоков время +увеличивается: добавляются накладные расходы на запуск и координацию MPI-процессов, которые на данной +постановке не окупаются, а многочисленные синхронизированные проходы сети Бэтчера ограничивают выигрыш от +дополнительных потоков. + +## 8. Conclusions + +ALL-версия даёт ускорение относительно baseline в конфигурации с двумя процессами и одним потоком. +Увеличение числа процессов и потоков снижает производительность из-за накладных расходов на управление +MPI и большого числа синхронизаций в сети Бэтчера. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +```cpp +bool TitaevSortirovkaBetcheraALL::RunImpl() { + auto &input = GetInput(); + const size_t n = input.size(); + if (n <= 1) { + return true; + } + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + ConvertFromKeys(keys, GetOutput()); + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + return true; +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/all/src/ops_all.cpp b/tasks/titaev_m_sortirovka_betchera/all/src/ops_all.cpp new file mode 100644 index 0000000000..2a0b266cf6 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/all/src/ops_all.cpp @@ -0,0 +1,173 @@ +#include "titaev_m_sortirovka_betchera/all/include/ops_all.hpp" + +#include + +#include +#include +#include +#include + +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +namespace { + +uint64_t DoubleToOrderedUint(double value) { + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(double)); + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits = ~bits; + } else { + bits ^= kSignMask; + } + return bits; +} + +double OrderedUintToDouble(uint64_t bits) { + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits ^= kSignMask; + } else { + bits = ~bits; + } + double result = 0.0; + std::memcpy(&result, &bits, sizeof(double)); + return result; +} + +} // namespace + +TitaevSortirovkaBetcheraALL::TitaevSortirovkaBetcheraALL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +bool TitaevSortirovkaBetcheraALL::ValidationImpl() { + return !GetInput().empty(); +} + +bool TitaevSortirovkaBetcheraALL::PreProcessingImpl() { + GetOutput() = GetInput(); + return true; +} + +void TitaevSortirovkaBetcheraALL::ConvertToKeys(const InType &input, std::vector &keys) { + const auto n = static_cast(input.size()); +#pragma omp parallel for default(none) shared(input, keys, n) + for (int64_t i = 0; i < n; i++) { + keys[i] = DoubleToOrderedUint(input[i]); + } +} + +void TitaevSortirovkaBetcheraALL::RadixSort(std::vector &keys) { + const std::size_t n = keys.size(); + if (n <= 1) { + return; + } + + constexpr int kBits = 8; + constexpr int kBuckets = 1 << kBits; + constexpr int kPasses = 64 / kBits; + + std::vector tmp(n); + const auto signed_n = static_cast(n); + + for (int pass = 0; pass < kPasses; pass++) { + std::vector count(kBuckets, 0); + const int num_threads = omp_get_max_threads(); + std::vector> local_count(num_threads, std::vector(kBuckets, 0)); + +#pragma omp parallel default(none) shared(keys, local_count, signed_n, pass) + { + const int tid = omp_get_thread_num(); + auto &lc = local_count[tid]; +#pragma omp for + for (int64_t i = 0; i < signed_n; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + lc[bucket]++; + } + } + + for (int bucket_idx = 0; bucket_idx < kBuckets; bucket_idx++) { + for (int thr = 0; thr < num_threads; thr++) { + count[bucket_idx] += local_count[thr][bucket_idx]; + } + } + + for (int i = 1; i < kBuckets; i++) { + count[i] += count[i - 1]; + } + + for (std::size_t i = n; i-- > 0;) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + tmp[--count[bucket]] = keys[i]; + } + + keys.swap(tmp); + } +} + +void TitaevSortirovkaBetcheraALL::ConvertFromKeys(const std::vector &keys, OutType &output) { + const auto n = static_cast(keys.size()); + output.resize(keys.size()); +#pragma omp parallel for default(none) shared(keys, output, n) + for (int64_t i = 0; i < n; i++) { + output[i] = OrderedUintToDouble(keys[i]); + } +} + +void TitaevSortirovkaBetcheraALL::BatcherStage(OutType &result, std::size_t array_size, std::size_t block, + std::size_t step) { + const auto signed_size = static_cast(array_size); +#pragma omp parallel for default(none) shared(result, signed_size, block, step) + for (int64_t i = 0; i < signed_size; i++) { + const auto idx = static_cast(i); + const std::size_t partner = idx ^ step; + if (partner <= idx) { + continue; + } + const bool ascending = ((idx & block) == 0); + const bool need_swap = ascending ? (result[idx] > result[partner]) : (result[idx] < result[partner]); + if (need_swap) { + std::swap(result[idx], result[partner]); + } + } +} + +void TitaevSortirovkaBetcheraALL::BatcherSort() { + auto &result = GetOutput(); + const std::size_t n = result.size(); + if (n < 2) { + return; + } + for (std::size_t block = 2; block <= n; block <<= 1) { + for (std::size_t step = block >> 1; step > 0; step >>= 1) { + BatcherStage(result, n, block, step); + } + } +} + +bool TitaevSortirovkaBetcheraALL::RunImpl() { + auto &input = GetInput(); + const std::size_t n = input.size(); + if (n <= 1) { + return true; + } + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + ConvertFromKeys(keys, GetOutput()); + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + return true; +} + +bool TitaevSortirovkaBetcheraALL::PostProcessingImpl() { + return true; +} + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp b/tasks/titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp index b7f3240864..033950a720 100644 --- a/tasks/titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp +++ b/tasks/titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp @@ -22,11 +22,10 @@ class TitaevSortirovkaBetcheraOMP : public BaseTask { bool PostProcessingImpl() override; static void ConvertToKeys(const InType &input, std::vector &keys); - static void RadixSortParallel(std::vector &keys); + static void RadixSort(std::vector &keys); static void ConvertFromKeys(const std::vector &keys, OutType &output); - void BatcherSortParallel(); - - static void BatcherStepParallel(OutType &result, size_t n, size_t step, size_t stage); + void BatcherSort(); + static void BatcherStage(OutType &result, std::size_t array_size, std::size_t block, std::size_t step); }; } // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/omp/report.md b/tasks/titaev_m_sortirovka_betchera/omp/report.md new file mode 100644 index 0000000000..dcc31ff4c6 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/omp/report.md @@ -0,0 +1,143 @@ +# Поразрядная сортировка для вещественных чисел с чётно-нечётным слиянием Бэтчера - OMP + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: OMP +- Variant: 20 + +## 1. Introduction + +OMP-версия ускоряет поразрядную сортировку массива `double` и сеть чётно-нечётного слияния Бэтчера за +счёт многопоточности OpenMP. Цель — распараллелить наиболее затратные по времени этапы и сравнить +производительность с последовательным baseline. + +## 2. Problem Statement + +Задача совпадает с остальными версиями: отсортировать массив `InType = std::vector` по +неубыванию, результат имеет тип `OutType = std::vector`. + +## 3. Baseline Algorithm (Sequential) + +Baseline описан в `seq/report.md`. Он выполняет преобразование `double` в упорядоченные 64-битные ключи, +поразрядную сортировку по основанию 256 и обратное преобразование, после чего при размере, равном степени +двойки, применяет сеть Бэтчера. + +## 4. Parallelization Scheme + +OMP-реализация распараллеливает следующие этапы: + +1. **Преобразование ключей:** цикл преобразования `double` в `uint64_t` распараллелен через +`#pragma omp parallel for`. +2. **Поразрядная сортировка:** на каждом проходе каждый поток заполняет собственную локальную гистограмму +корзин, затем гистограммы суммируются, и выполняется стабильное распределение. +3. **Сеть Бэтчера:** на каждом шаге сравнения пары индексов обрабатываются параллельным циклом +`#pragma omp parallel for`; пары на одном шаге не пересекаются, поэтому гонок нет. + +Число потоков задаётся через `PPC_NUM_THREADS`. + +## 5. Implementation Details + +- Файлы: `omp/include/ops_omp.hpp`, `omp/src/ops_omp.cpp`. +- Класс: `TitaevSortirovkaBetcheraOMP`. +- Локальные гистограммы исключают гонки при подсчёте корзин. +- Распределение элементов по корзинам выполняется последовательно для сохранения стабильности. + +Память: один временный массив ключей и по одной гистограмме на каждый поток. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на упорядоченность по неубыванию. Результаты OMP-версии совпадают с baseline на всех тестовых +наборах. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | +| omp | 2 | 0.056191 | 2.85 | 142.34% | +| omp | 4 | 0.063745 | 2.51 | 62.73% | +| omp | 8 | 0.080437 | 1.99 | 24.86% | + +Лучший результат достигается на двух потоках. С ростом числа потоков время увеличивается: сеть Бэтчера +выполняет порядка нескольких сотен синхронизированных проходов по массиву, и накладные расходы на барьеры +между проходами начинают преобладать над выигрышем от параллелизма. + +## 8. Conclusions + +OMP-версия ускоряет сортировку относительно baseline, наибольшее ускорение получено на двух потоках. +Дальнейшее увеличение числа потоков снижает эффективность из-за большого количества синхронизаций в сети +Бэтчера. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +```cpp +void TitaevSortirovkaBetcheraOMP::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { +#pragma omp parallel for + for (long long ii = 0; ii < static_cast(n); ii++) { + const size_t i = static_cast(ii); + const size_t l = i ^ j; + if (l > i) { + const bool ascending = ((i & k) == 0); + const bool need_swap = ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + } + } +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/omp/src/ops_omp.cpp b/tasks/titaev_m_sortirovka_betchera/omp/src/ops_omp.cpp index 444c98bf88..17aba62074 100644 --- a/tasks/titaev_m_sortirovka_betchera/omp/src/ops_omp.cpp +++ b/tasks/titaev_m_sortirovka_betchera/omp/src/ops_omp.cpp @@ -12,77 +12,31 @@ namespace titaev_m_sortirovka_betchera { namespace { + uint64_t DoubleToOrderedUint(double value) { - uint64_t x = 0; - std::memcpy(&x, &value, sizeof(double)); + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(double)); constexpr uint64_t kSignMask = (1ULL << 63); - if ((x & kSignMask) != 0ULL) { - x = ~x; + if ((bits & kSignMask) != 0ULL) { + bits = ~bits; } else { - x ^= kSignMask; + bits ^= kSignMask; } - return x; + return bits; } -double OrderedUintToDouble(uint64_t x) { +double OrderedUintToDouble(uint64_t bits) { constexpr uint64_t kSignMask = (1ULL << 63); - if ((x & kSignMask) != 0ULL) { - x ^= kSignMask; + if ((bits & kSignMask) != 0ULL) { + bits ^= kSignMask; } else { - x = ~x; + bits = ~bits; } double result = 0.0; - std::memcpy(&result, &x, sizeof(double)); + std::memcpy(&result, &bits, sizeof(double)); return result; } -void RadixPass(int pass, size_t n, const std::vector &src, std::vector &dest) { - constexpr int kBits = 8; - constexpr int kBuckets = 256; - int num_threads = omp_get_max_threads(); - std::vector> local_counts(num_threads, std::vector(kBuckets, 0)); - -#pragma omp parallel default(none) shared(src, local_counts, n, pass, kBits) - { - int tid = omp_get_thread_num(); -#pragma omp for - for (size_t i = 0; i < n; ++i) { - size_t bucket = (src[i] >> (static_cast(pass) * kBits)) & 255; - local_counts[static_cast(tid)][bucket]++; - } - } - - std::vector common_counts(kBuckets, 0); - for (const auto &l_count : local_counts) { - for (size_t b_idx = 0; b_idx < kBuckets; ++b_idx) { - common_counts[b_idx] += l_count[b_idx]; - } - } - - std::vector prefixes(kBuckets, 0); - for (size_t b_idx = 1; b_idx < kBuckets; ++b_idx) { - prefixes[b_idx] = prefixes[b_idx - 1] + common_counts[b_idx - 1]; - } - - std::vector> offsets(num_threads, std::vector(kBuckets)); - for (size_t b_idx = 0; b_idx < kBuckets; ++b_idx) { - size_t curr = prefixes[b_idx]; - for (int t_idx = 0; t_idx < num_threads; ++t_idx) { - offsets[static_cast(t_idx)][b_idx] = curr; - curr += local_counts[static_cast(t_idx)][b_idx]; - } - } - -#pragma omp parallel default(none) shared(src, dest, offsets, n, pass, kBits) - { - int tid = omp_get_thread_num(); -#pragma omp for - for (size_t i = 0; i < n; ++i) { - size_t bucket = (src[i] >> (static_cast(pass) * kBits)) & 255; - dest[offsets[static_cast(tid)][bucket]++] = src[i]; - } - } -} } // namespace TitaevSortirovkaBetcheraOMP::TitaevSortirovkaBetcheraOMP(const InType &in) { @@ -101,74 +55,113 @@ bool TitaevSortirovkaBetcheraOMP::PreProcessingImpl() { } void TitaevSortirovkaBetcheraOMP::ConvertToKeys(const InType &input, std::vector &keys) { - const size_t n = input.size(); -#pragma omp parallel for default(none) shared(keys, input, n) - for (size_t i = 0; i < n; i++) { + const auto n = static_cast(input.size()); +#pragma omp parallel for default(none) shared(input, keys, n) + for (int64_t i = 0; i < n; i++) { keys[i] = DoubleToOrderedUint(input[i]); } } -void TitaevSortirovkaBetcheraOMP::RadixSortParallel(std::vector &keys) { - const size_t n = keys.size(); +void TitaevSortirovkaBetcheraOMP::RadixSort(std::vector &keys) { + const std::size_t n = keys.size(); if (n <= 1) { return; } + + constexpr int kBits = 8; + constexpr int kBuckets = 1 << kBits; + constexpr int kPasses = 64 / kBits; + std::vector tmp(n); - for (int pass = 0; pass < 8; ++pass) { - if (pass % 2 == 0) { - RadixPass(pass, n, keys, tmp); - } else { - RadixPass(pass, n, tmp, keys); + const auto signed_n = static_cast(n); + + for (int pass = 0; pass < kPasses; pass++) { + std::vector count(kBuckets, 0); + const int num_threads = omp_get_max_threads(); + std::vector> local_count(num_threads, std::vector(kBuckets, 0)); + +#pragma omp parallel default(none) shared(keys, local_count, signed_n, pass) + { + const int tid = omp_get_thread_num(); + auto &lc = local_count[tid]; +#pragma omp for + for (int64_t i = 0; i < signed_n; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + lc[bucket]++; + } + } + + for (int bucket_idx = 0; bucket_idx < kBuckets; bucket_idx++) { + for (int thr = 0; thr < num_threads; thr++) { + count[bucket_idx] += local_count[thr][bucket_idx]; + } + } + + for (int i = 1; i < kBuckets; i++) { + count[i] += count[i - 1]; + } + + for (std::size_t i = n; i-- > 0;) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + tmp[--count[bucket]] = keys[i]; } + + keys.swap(tmp); } } void TitaevSortirovkaBetcheraOMP::ConvertFromKeys(const std::vector &keys, OutType &output) { - const size_t n = keys.size(); - output.resize(n); + const auto n = static_cast(keys.size()); + output.resize(keys.size()); #pragma omp parallel for default(none) shared(keys, output, n) - for (size_t i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; i++) { output[i] = OrderedUintToDouble(keys[i]); } } -void TitaevSortirovkaBetcheraOMP::BatcherStepParallel(OutType &result, size_t n, size_t step, size_t stage) { -#pragma omp parallel for default(none) shared(result, n, step, stage) - for (size_t i = 0; i < n; ++i) { - size_t j = i ^ stage; - if (j > i && j < n) { - const bool ascending = (i & step) == 0; - if (ascending ? (result[i] > result[j]) : (result[i] < result[j])) { - std::swap(result[i], result[j]); - } +void TitaevSortirovkaBetcheraOMP::BatcherStage(OutType &result, std::size_t array_size, std::size_t block, + std::size_t step) { + const auto signed_size = static_cast(array_size); +#pragma omp parallel for default(none) shared(result, signed_size, block, step) + for (int64_t i = 0; i < signed_size; i++) { + const auto idx = static_cast(i); + const std::size_t partner = idx ^ step; + if (partner <= idx) { + continue; + } + const bool ascending = ((idx & block) == 0); + const bool need_swap = ascending ? (result[idx] > result[partner]) : (result[idx] < result[partner]); + if (need_swap) { + std::swap(result[idx], result[partner]); } } } -void TitaevSortirovkaBetcheraOMP::BatcherSortParallel() { +void TitaevSortirovkaBetcheraOMP::BatcherSort() { auto &result = GetOutput(); - const size_t n = result.size(); - for (size_t step = 1; step < n; step <<= 1) { - for (size_t stage = step; stage > 0; stage >>= 1) { - BatcherStepParallel(result, n, step, stage); + const std::size_t n = result.size(); + if (n < 2) { + return; + } + for (std::size_t block = 2; block <= n; block <<= 1) { + for (std::size_t step = block >> 1; step > 0; step >>= 1) { + BatcherStage(result, n, block, step); } } } bool TitaevSortirovkaBetcheraOMP::RunImpl() { auto &input = GetInput(); - const size_t n = input.size(); + const std::size_t n = input.size(); if (n <= 1) { return true; } - std::vector keys(n); ConvertToKeys(input, keys); - RadixSortParallel(keys); + RadixSort(keys); ConvertFromKeys(keys, GetOutput()); - if ((n & (n - 1)) == 0) { - BatcherSortParallel(); + BatcherSort(); } return true; } diff --git a/tasks/titaev_m_sortirovka_betchera/report.md b/tasks/titaev_m_sortirovka_betchera/report.md new file mode 100644 index 0000000000..25a9cb2bf2 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/report.md @@ -0,0 +1,308 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с чётно-нечётным слиянием Бэтчера + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: SEQ, OMP, TBB, STL, ALL +- Variant: 20 + +## 1. Introduction + +Работа посвящена сортировке массива вещественных чисел типа `double` с помощью поразрядной сортировки +(radix sort) и последующего применения сети чётно-нечётного слияния Бэтчера. Поразрядная сортировка +обеспечивает линейную по числу элементов сложность за счёт обработки чисел по разрядам, а сеть Бэтчера +служит детерминированной сортирующей сетью, удобной для параллельной реализации. В работе реализованы +пять вариантов одной задачи: последовательный baseline и параллельные версии на OMP, TBB, STL и ALL. + +Ожидаемый результат работы — корректно отсортированный массив и сравнение времени выполнения разных +технологий параллелизма на одинаковой постановке задачи. + +## 2. Problem Statement + +Входные данные задаются типом `InType = std::vector` — массив вещественных чисел. + +Выходные данные: `OutType = std::vector` — тот же массив, отсортированный по неубыванию. + +Ключевая идея — преобразовать каждое число `double` в беззнаковый 64-битный ключ с сохранением порядка, +отсортировать ключи поразрядной сортировкой и при размере, равном степени двойки, применить сеть Бэтчера. +Ограничение: входной массив не должен быть пустым. + +## 3. Baseline Algorithm (Sequential) + +Последовательная версия служит baseline для всех остальных реализаций. Алгоритм состоит из трёх этапов. + +На первом этапе каждое число `double` преобразуется в 64-битный беззнаковый ключ `uint64_t` так, чтобы +лексикографический порядок ключей совпадал с числовым порядком исходных чисел. Для этого у положительных +чисел инвертируется знаковый бит, а у отрицательных инвертируются все биты. + +На втором этапе выполняется поразрядная сортировка ключей по основанию 256 (по 8 бит за проход, всего +8 проходов на 64 бита). Каждый проход использует счётную сортировку: подсчёт количества элементов в +корзинах, префиксные суммы и стабильное распределение по временному массиву. + +На третьем этапе ключи преобразуются обратно в `double`. Если размер массива является степенью двойки, +дополнительно применяется сеть чётно-нечётного слияния Бэтчера, реализованная как битоническая +сортировка с обменами по индексам, отличающимся на один бит. + +## 4. Parallelization Scheme + +- SEQ: один поток, параллелизм не используется. +- OMP: параллельное преобразование ключей, подсчёт корзин с локальными гистограммами по потокам и + параллельные проходы сети Бэтчера через `#pragma omp parallel for`. +- TBB: `oneapi::tbb::parallel_for` по `blocked_range` на этапах преобразования и в сети Бэтчера. +- STL: ручное разбиение диапазона между `std::thread`, локальные гистограммы и объединение после + `join()`. +- ALL: MPI-окружение для запуска под несколькими процессами, внутри каждого процесса OpenMP-параллелизм + на этапах поразрядной сортировки и сети Бэтчера. + +## 5. Implementation Details + +Код задачи расположен в папке `tasks/titaev_m_sortirovka_betchera`: + +- `common/include/common.hpp` — типы `InType`, `OutType`, `TestType`, `BaseTask`; +- `seq/src/ops_seq.cpp` — последовательный baseline; +- `omp/src/ops_omp.cpp` — OpenMP-версия; +- `tbb/src/ops_tbb.cpp` — TBB-версия; +- `stl/src/ops_stl.cpp` — STL-версия; +- `all/src/ops_all.cpp` — гибридная версия ALL; +- `tests/functional/main.cpp` — функциональные тесты; +- `tests/performance/main.cpp` — performance-тесты. + +Память используется экономно: для поразрядной сортировки выделяется один временный массив ключей и массив +счётчиков фиксированного размера на 256 корзин. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на полную упорядоченность по неубыванию, а его размер сравнивается с размером входа. +Тестовые наборы включают как степени двойки, так и произвольные размеры, что покрывает оба пути +алгоритма: только поразрядную сортировку и поразрядную сортировку с последующим слиянием Бэтчера. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | +| omp | 2 | 0.056191 | 2.85 | 142.34% | +| omp | 4 | 0.063745 | 2.51 | 62.73% | +| omp | 8 | 0.080437 | 1.99 | 24.86% | +| tbb | 2 | 0.123646 | 1.29 | 64.69% | +| tbb | 4 | 0.074043 | 2.16 | 54.01% | +| tbb | 8 | 0.063984 | 2.50 | 31.25% | +| stl | 2 | 0.179218 | 0.89 | 44.62% | +| stl | 4 | 0.164399 | 0.97 | 24.33% | +| stl | 8 | 0.165794 | 0.96 | 12.06% | +| all | 2 × 1 | 0.079706 | 2.01 | 100.33% | +| all | 2 × 2 | 0.099306 | 1.61 | 40.27% | +| all | 4 × 2 | 0.227350 | 0.70 | 8.79% | + +OMP даёт наибольшее ускорение на двух потоках и теряет эффективность при дальнейшем росте числа потоков. +TBB масштабируется монотонно и приближается к результату OMP на восьми потоках. STL не показывает +ускорения, поскольку число рабочих потоков определяется аппаратной конфигурацией, а не заданным +параметром, и потоки пересоздаются на каждом шаге сети Бэтчера. ALL даёт ускорение в конфигурации +`2 × 1`, но проседает при росте числа процессов из-за накладных расходов на координацию MPI. Общая +причина ограниченной масштабируемости — большое число синхронизированных проходов в сети Бэтчера, +растущее логарифмически от размера массива. + +## 8. Conclusions + +Реализация сортирует массив `double` через поразрядную сортировку и сеть чётно-нечётного слияния Бэтчера. +Последовательная версия используется как baseline, а параллельные версии сравниваются с ней по времени +выполнения, ускорению и эффективности. Наилучшее ускорение получено у OMP на двух потоках и у ALL в +конфигурации `2 × 1`. Основное ограничение масштабируемости связано с большим числом точек синхронизации +в сети Бэтчера и накладными расходами на управление параллелизмом. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +Ниже приведены основные фрагменты `RunImpl()` и сети Бэтчера для всех реализаций. + +### SEQ RunImpl + +```cpp +bool TitaevSortirovkaBetcheraSEQ::RunImpl() { + auto &input = GetInput(); + const size_t n = input.size(); + if (n <= 1) { + return true; + } + + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + + ConvertFromKeys(keys, GetOutput()); + + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + + return true; +} +``` + +### OMP BatcherSort + +```cpp +void TitaevSortirovkaBetcheraOMP::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { +#pragma omp parallel for + for (long long ii = 0; ii < static_cast(n); ii++) { + const size_t i = static_cast(ii); + const size_t l = i ^ j; + if (l > i) { + const bool ascending = ((i & k) == 0); + const bool need_swap = ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + } + } +} +``` + +### TBB BatcherSort + +```cpp +void TitaevSortirovkaBetcheraTBB::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { + oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0, n), + [&](const oneapi::tbb::blocked_range &r) { + for (size_t i = r.begin(); i < r.end(); i++) { + const size_t l = i ^ j; + if (l > i && l < n) { + const bool ascending = ((i & k) == 0); + const bool need_swap = + ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + }); + } + } +} +``` + +### STL BatcherSort + +```cpp +void TitaevSortirovkaBetcheraSTL::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + const unsigned int num_threads = GetThreadCount(); + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { + std::vector threads; + threads.reserve(num_threads); + const size_t chunk = (n + num_threads - 1) / num_threads; + + for (unsigned int t = 0; t < num_threads; t++) { + const size_t begin = t * chunk; + const size_t end = std::min(begin + chunk, n); + if (begin >= end) { + break; + } + threads.emplace_back([&result, n, k, j, begin, end]() { + for (size_t i = begin; i < end; i++) { + const size_t l = i ^ j; + if (l > i && l < n) { + const bool ascending = ((i & k) == 0); + const bool need_swap = ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + }); + } + for (auto &th : threads) { + th.join(); + } + } + } +} +``` + +### ALL RunImpl + +```cpp +bool TitaevSortirovkaBetcheraALL::RunImpl() { + auto &input = GetInput(); + const size_t n = input.size(); + if (n <= 1) { + return true; + } + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + ConvertFromKeys(keys, GetOutput()); + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + return true; +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp b/tasks/titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp index 85df666bb0..141bea5de4 100644 --- a/tasks/titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp +++ b/tasks/titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp @@ -25,8 +25,7 @@ class TitaevSortirovkaBetcheraSEQ : public BaseTask { static void RadixSort(std::vector &keys); static void ConvertFromKeys(const std::vector &keys, OutType &output); void BatcherSort(); - - static void BatcherStep(OutType &result, size_t n, size_t step, size_t stage); + static void BatcherStage(OutType &result, std::size_t array_size, std::size_t block, std::size_t step); }; } // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/seq/report.md b/tasks/titaev_m_sortirovka_betchera/seq/report.md new file mode 100644 index 0000000000..018eb5ff0d --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/seq/report.md @@ -0,0 +1,154 @@ +# Поразрядная сортировка для вещественных чисел с чётно-нечётным слиянием Бэтчера - SEQ + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: SEQ +- Variant: 20 + +## 1. Introduction + +Работа посвящена сортировке массива вещественных чисел типа `double` с помощью поразрядной сортировки +(radix sort) и последующего применения сети чётно-нечётного слияния Бэтчера. Поразрядная сортировка +обеспечивает линейную по числу элементов сложность за счёт обработки чисел по разрядам, а сеть Бэтчера +служит детерминированной сортирующей сетью, удобной для параллельной реализации. + +В работе реализованы пять вариантов одной задачи: последовательный baseline и параллельные версии на +OMP, TBB, STL и ALL. SEQ-версия используется как эталон корректности и точка отсчёта для измерения +ускорения. + +## 2. Problem Statement + +Входные данные задаются типом `InType = std::vector` — массив вещественных чисел. + +Выходные данные: `OutType = std::vector` — тот же массив, отсортированный по неубыванию. + +Требуется получить корректно отсортированную последовательность для произвольного входного массива. +Ограничение: массив не должен быть пустым. + +## 3. Baseline Algorithm (Sequential) + +Последовательная версия служит baseline для всех остальных реализаций. Алгоритм состоит из трёх этапов. + +На первом этапе каждое число `double` преобразуется в 64-битный беззнаковый ключ `uint64_t` так, чтобы +лексикографический порядок ключей совпадал с числовым порядком исходных чисел. Для этого у положительных +чисел инвертируется знаковый бит, а у отрицательных инвертируются все биты. + +На втором этапе выполняется поразрядная сортировка ключей по основанию 256 (по 8 бит за проход, всего +8 проходов на 64 бита). Каждый проход использует счётную сортировку: подсчёт количества элементов в +корзинах, префиксные суммы и стабильное распределение по временному массиву. + +На третьем этапе ключи преобразуются обратно в `double`. Если размер массива является степенью двойки, +дополнительно применяется сеть чётно-нечётного слияния Бэтчера, реализованная как битоническая +сортировка с обменами по индексам, отличающимся на один бит. + +## 4. Parallelization Scheme + +- SEQ: один поток, параллелизм не используется. +- OMP: распараллеливание подсчёта корзин (локальные гистограммы по потокам) и проходов сети Бэтчера. +- TBB: `oneapi::tbb::parallel_for` по `blocked_range` на этапах преобразования и в сети Бэтчера. +- STL: ручное разбиение диапазона между `std::thread` с локальными гистограммами. +- ALL: MPI-окружение плюс OpenMP-параллелизм внутри процесса на этапах сортировки и слияния. + +## 5. Implementation Details + +Код задачи расположен в папке `tasks/titaev_m_sortirovka_betchera`: + +- `common/include/common.hpp` — типы `InType`, `OutType`, `TestType`, `BaseTask`; +- `seq/src/ops_seq.cpp` — последовательный baseline; +- `omp/src/ops_omp.cpp` — OpenMP-версия; +- `tbb/src/ops_tbb.cpp` — TBB-версия; +- `stl/src/ops_stl.cpp` — STL-версия; +- `all/src/ops_all.cpp` — гибридная версия ALL; +- `tests/functional/main.cpp` — функциональные тесты; +- `tests/performance/main.cpp` — performance-тесты. + +Память используется экономно: для поразрядной сортировки выделяется один временный массив ключей и массив +счётчиков фиксированного размера на 256 корзин. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на полную упорядоченность по неубыванию, а его размер сравнивается с размером входа. +Тестовые наборы включают как степени двойки, так и произвольные размеры, что покрывает оба пути +алгоритма: только поразрядную сортировку и поразрядную сортировку с последующим слиянием Бэтчера. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | + +## 8. Conclusions + +Последовательная версия реализует сортировку массива `double` через поразрядную сортировку и сеть +чётно-нечётного слияния Бэтчера. Она используется как baseline, относительно которого оцениваются +параллельные версии. Основное ограничение масштабируемости связано с большим числом синхронизированных +проходов в сети Бэтчера, которое растёт логарифмически от размера массива. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +```cpp +bool TitaevSortirovkaBetcheraSEQ::RunImpl() { + auto &input = GetInput(); + const size_t n = input.size(); + if (n <= 1) { + return true; + } + + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + + ConvertFromKeys(keys, GetOutput()); + + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + + return true; +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/seq/src/ops_seq.cpp b/tasks/titaev_m_sortirovka_betchera/seq/src/ops_seq.cpp index 5fa12d4331..0525be97c6 100644 --- a/tasks/titaev_m_sortirovka_betchera/seq/src/ops_seq.cpp +++ b/tasks/titaev_m_sortirovka_betchera/seq/src/ops_seq.cpp @@ -12,33 +12,29 @@ namespace titaev_m_sortirovka_betchera { namespace { uint64_t DoubleToOrderedUint(double value) { - uint64_t x = 0; - std::memcpy(&x, &value, sizeof(double)); - + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(double)); constexpr uint64_t kSignMask = (1ULL << 63); - - if ((x & kSignMask) != 0ULL) { - x = ~x; + if ((bits & kSignMask) != 0ULL) { + bits = ~bits; } else { - x ^= kSignMask; + bits ^= kSignMask; } - - return x; + return bits; } -double OrderedUintToDouble(uint64_t x) { +double OrderedUintToDouble(uint64_t bits) { constexpr uint64_t kSignMask = (1ULL << 63); - - if ((x & kSignMask) != 0ULL) { - x ^= kSignMask; + if ((bits & kSignMask) != 0ULL) { + bits ^= kSignMask; } else { - x = ~x; + bits = ~bits; } - double result = 0.0; - std::memcpy(&result, &x, sizeof(double)); + std::memcpy(&result, &bits, sizeof(double)); return result; } + } // namespace TitaevSortirovkaBetcheraSEQ::TitaevSortirovkaBetcheraSEQ(const InType &in) { @@ -57,14 +53,14 @@ bool TitaevSortirovkaBetcheraSEQ::PreProcessingImpl() { } void TitaevSortirovkaBetcheraSEQ::ConvertToKeys(const InType &input, std::vector &keys) { - const size_t n = input.size(); - for (size_t i = 0; i < n; i++) { + const std::size_t n = input.size(); + for (std::size_t i = 0; i < n; i++) { keys[i] = DoubleToOrderedUint(input[i]); } } void TitaevSortirovkaBetcheraSEQ::RadixSort(std::vector &keys) { - const size_t n = keys.size(); + const std::size_t n = keys.size(); if (n <= 1) { return; } @@ -76,10 +72,10 @@ void TitaevSortirovkaBetcheraSEQ::RadixSort(std::vector &keys) { std::vector tmp(n); for (int pass = 0; pass < kPasses; pass++) { - std::vector count(kBuckets, 0); + std::vector count(kBuckets, 0); - for (size_t i = 0; i < n; i++) { - size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + for (std::size_t i = 0; i < n; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); count[bucket]++; } @@ -87,8 +83,8 @@ void TitaevSortirovkaBetcheraSEQ::RadixSort(std::vector &keys) { count[i] += count[i - 1]; } - for (size_t i = n; i-- > 0;) { - size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + for (std::size_t i = n; i-- > 0;) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); tmp[--count[bucket]] = keys[i]; } @@ -97,42 +93,44 @@ void TitaevSortirovkaBetcheraSEQ::RadixSort(std::vector &keys) { } void TitaevSortirovkaBetcheraSEQ::ConvertFromKeys(const std::vector &keys, OutType &output) { - const size_t n = keys.size(); + const std::size_t n = keys.size(); output.resize(n); - for (size_t i = 0; i < n; i++) { + for (std::size_t i = 0; i < n; i++) { output[i] = OrderedUintToDouble(keys[i]); } } -void TitaevSortirovkaBetcheraSEQ::BatcherStep(OutType &result, size_t n, size_t step, size_t stage) { - for (size_t i = 0; i < n; i++) { - size_t j = i ^ stage; - if (j <= i || j >= n) { +void TitaevSortirovkaBetcheraSEQ::BatcherStage(OutType &result, std::size_t array_size, std::size_t block, + std::size_t step) { + for (std::size_t i = 0; i < array_size; i++) { + const std::size_t partner = i ^ step; + if (partner <= i) { continue; } - - const bool ascending = (i & step) == 0; - const bool need_swap = ascending ? result[i] > result[j] : result[i] < result[j]; + const bool ascending = ((i & block) == 0); + const bool need_swap = ascending ? (result[i] > result[partner]) : (result[i] < result[partner]); if (need_swap) { - std::swap(result[i], result[j]); + std::swap(result[i], result[partner]); } } } void TitaevSortirovkaBetcheraSEQ::BatcherSort() { auto &result = GetOutput(); - const size_t n = result.size(); - - for (size_t step = 1; step < n; step <<= 1) { - for (size_t stage = step; stage > 0; stage >>= 1) { - BatcherStep(result, n, step, stage); + const std::size_t n = result.size(); + if (n < 2) { + return; + } + for (std::size_t block = 2; block <= n; block <<= 1) { + for (std::size_t step = block >> 1; step > 0; step >>= 1) { + BatcherStage(result, n, block, step); } } } bool TitaevSortirovkaBetcheraSEQ::RunImpl() { auto &input = GetInput(); - const size_t n = input.size(); + const std::size_t n = input.size(); if (n <= 1) { return true; } diff --git a/tasks/titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp b/tasks/titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp new file mode 100644 index 0000000000..fcf057fc9a --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include + +#include "task/include/task.hpp" +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +class TitaevSortirovkaBetcheraSTL : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSTL; + } + explicit TitaevSortirovkaBetcheraSTL(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static void ConvertToKeys(const InType &input, std::vector &keys); + static void RadixSort(std::vector &keys); + static void CountSequential(const std::vector &keys, std::vector &count, int pass); + static void CountParallel(const std::vector &keys, std::vector &count, int pass, + unsigned int num_threads); + static void RadixCountPass(std::vector &keys, std::vector &tmp, int pass); + static void ConvertFromKeys(const std::vector &keys, OutType &output); + void BatcherSort(); + static void BatcherStage(OutType &result, std::size_t array_size, std::size_t block, std::size_t step); +}; + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/stl/report.md b/tasks/titaev_m_sortirovka_betchera/stl/report.md new file mode 100644 index 0000000000..4f1e924327 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/stl/report.md @@ -0,0 +1,161 @@ +# Поразрядная сортировка для вещественных чисел с чётно-нечётным слиянием Бэтчера - STL + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: STL +- Variant: 20 + +## 1. Introduction + +STL-версия реализует параллелизм с помощью стандартной библиотеки потоков `std::thread`. Цель — вручную +разбить работу между потоками на этапах преобразования данных и сети чётно-нечётного слияния Бэтчера и +сравнить производительность с baseline. + +## 2. Problem Statement + +Задача совпадает с остальными версиями: отсортировать массив `InType = std::vector` по +неубыванию, результат имеет тип `OutType = std::vector`. + +## 3. Baseline Algorithm (Sequential) + +Baseline описан в `seq/report.md`. Он выполняет преобразование `double` в упорядоченные 64-битные ключи, +поразрядную сортировку по основанию 256 и обратное преобразование, после чего при размере, равном степени +двойки, применяет сеть Бэтчера. + +## 4. Parallelization Scheme + +STL-реализация распараллеливает следующие этапы: + +1. **Преобразование ключей:** диапазон индексов делится на непрерывные блоки, каждый обрабатывается +отдельным `std::thread`. +2. **Поразрядная сортировка:** на каждом проходе каждый поток заполняет собственную локальную гистограмму +корзин на своём блоке; гистограммы суммируются, после чего выполняется стабильное распределение. +3. **Сеть Бэтчера:** на каждом шаге сравнения диапазон индексов делится между потоками; пары индексов на +одном шаге не пересекаются, поэтому гонок нет. + +Объединение результатов выполняется после `join()` всех потоков. + +## 5. Implementation Details + +- Файлы: `stl/include/ops_stl.hpp`, `stl/src/ops_stl.cpp`. +- Класс: `TitaevSortirovkaBetcheraSTL`. +- Число потоков определяется через `std::thread::hardware_concurrency()`. +- Локальные гистограммы и поблочное разбиение исключают гонки данных. + +Память: один временный массив ключей, по одной гистограмме на каждый поток и вектор объектов потоков. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на упорядоченность по неубыванию. Результаты STL-версии совпадают с baseline на всех тестовых +наборах. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | +| stl | 2 | 0.179218 | 0.89 | 44.62% | +| stl | 4 | 0.164399 | 0.97 | 24.33% | +| stl | 8 | 0.165794 | 0.96 | 12.06% | + +Время STL-версии слабо зависит от заданного числа потоков, поскольку число рабочих потоков определяется +аппаратной конфигурацией процессора, а не переменной окружения. На больших массивах сеть Бэтчера создаёт +потоки в каждом из множества проходов, и затраты на создание и объединение потоков превышают выигрыш от +распараллеливания, поэтому ускорение относительно baseline отсутствует. + +## 8. Conclusions + +STL-версия на данной постановке не даёт ускорения относительно baseline. Основная причина — +многократное создание потоков на каждом шаге сети Бэтчера и независимость числа потоков от заданного +параметра. Для улучшения масштабируемости потребовался бы пул потоков и сокращение числа точек +синхронизации. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +```cpp +void TitaevSortirovkaBetcheraSTL::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + const unsigned int num_threads = GetThreadCount(); + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { + std::vector threads; + threads.reserve(num_threads); + const size_t chunk = (n + num_threads - 1) / num_threads; + + for (unsigned int t = 0; t < num_threads; t++) { + const size_t begin = t * chunk; + const size_t end = std::min(begin + chunk, n); + if (begin >= end) { + break; + } + threads.emplace_back([&result, n, k, j, begin, end]() { + for (size_t i = begin; i < end; i++) { + const size_t l = i ^ j; + if (l > i && l < n) { + const bool ascending = ((i & k) == 0); + const bool need_swap = ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + }); + } + for (auto &th : threads) { + th.join(); + } + } + } +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/stl/src/ops_stl.cpp b/tasks/titaev_m_sortirovka_betchera/stl/src/ops_stl.cpp new file mode 100644 index 0000000000..3e2c6eaf63 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/stl/src/ops_stl.cpp @@ -0,0 +1,233 @@ +#include "titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +namespace { + +constexpr int kBits = 8; +constexpr int kBuckets = 1 << kBits; +constexpr int kPasses = 64 / kBits; +constexpr std::size_t kSequentialThreshold = 1 << 16; + +uint64_t DoubleToOrderedUint(double value) { + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(double)); + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits = ~bits; + } else { + bits ^= kSignMask; + } + return bits; +} + +double OrderedUintToDouble(uint64_t bits) { + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits ^= kSignMask; + } else { + bits = ~bits; + } + double result = 0.0; + std::memcpy(&result, &bits, sizeof(double)); + return result; +} + +unsigned int GetThreadCount() { + const unsigned int hw = std::thread::hardware_concurrency(); + return hw == 0 ? 1U : hw; +} + +void RunInParallel(std::size_t total, const std::function &body) { + const unsigned int num_threads = (total < kSequentialThreshold) ? 1U : GetThreadCount(); + + if (num_threads <= 1) { + body(0, total); + return; + } + + std::vector threads; + threads.reserve(num_threads); + const std::size_t chunk = (total + num_threads - 1) / num_threads; + + for (unsigned int thr = 0; thr < num_threads; thr++) { + const std::size_t begin = thr * chunk; + const std::size_t end = std::min(begin + chunk, total); + if (begin >= end) { + break; + } + threads.emplace_back([&body, begin, end]() { body(begin, end); }); + } + for (auto &th : threads) { + th.join(); + } +} + +} // namespace + +TitaevSortirovkaBetcheraSTL::TitaevSortirovkaBetcheraSTL(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +bool TitaevSortirovkaBetcheraSTL::ValidationImpl() { + return !GetInput().empty(); +} + +bool TitaevSortirovkaBetcheraSTL::PreProcessingImpl() { + GetOutput() = GetInput(); + return true; +} + +void TitaevSortirovkaBetcheraSTL::ConvertToKeys(const InType &input, std::vector &keys) { + RunInParallel(input.size(), [&input, &keys](std::size_t begin, std::size_t end) { + for (std::size_t i = begin; i < end; i++) { + keys[i] = DoubleToOrderedUint(input[i]); + } + }); +} + +void TitaevSortirovkaBetcheraSTL::CountSequential(const std::vector &keys, std::vector &count, + int pass) { + const std::size_t n = keys.size(); + for (std::size_t i = 0; i < n; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + count[bucket]++; + } +} + +void TitaevSortirovkaBetcheraSTL::CountParallel(const std::vector &keys, std::vector &count, + int pass, unsigned int num_threads) { + const std::size_t n = keys.size(); + std::vector> local_count(num_threads, std::vector(kBuckets, 0)); + const std::size_t chunk = (n + num_threads - 1) / num_threads; + + std::vector threads; + threads.reserve(num_threads); + for (unsigned int thr = 0; thr < num_threads; thr++) { + const std::size_t begin = thr * chunk; + const std::size_t end = std::min(begin + chunk, n); + if (begin >= end) { + break; + } + threads.emplace_back([&keys, &local_count, thr, begin, end, pass]() { + auto &lc = local_count[thr]; + for (std::size_t i = begin; i < end; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + lc[bucket]++; + } + }); + } + for (auto &th : threads) { + th.join(); + } + + for (int bucket_idx = 0; bucket_idx < kBuckets; bucket_idx++) { + for (unsigned int thr = 0; thr < num_threads; thr++) { + count[bucket_idx] += local_count[thr][bucket_idx]; + } + } +} + +void TitaevSortirovkaBetcheraSTL::RadixCountPass(std::vector &keys, std::vector &tmp, int pass) { + const std::size_t n = keys.size(); + const unsigned int num_threads = (n < kSequentialThreshold) ? 1U : GetThreadCount(); + + std::vector count(kBuckets, 0); + if (num_threads <= 1) { + CountSequential(keys, count, pass); + } else { + CountParallel(keys, count, pass, num_threads); + } + + for (int i = 1; i < kBuckets; i++) { + count[i] += count[i - 1]; + } + for (std::size_t i = n; i-- > 0;) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + tmp[--count[bucket]] = keys[i]; + } + keys.swap(tmp); +} +void TitaevSortirovkaBetcheraSTL::RadixSort(std::vector &keys) { + const std::size_t n = keys.size(); + if (n <= 1) { + return; + } + std::vector tmp(n); + for (int pass = 0; pass < kPasses; pass++) { + RadixCountPass(keys, tmp, pass); + } +} + +void TitaevSortirovkaBetcheraSTL::ConvertFromKeys(const std::vector &keys, OutType &output) { + output.resize(keys.size()); + RunInParallel(keys.size(), [&keys, &output](std::size_t begin, std::size_t end) { + for (std::size_t i = begin; i < end; i++) { + output[i] = OrderedUintToDouble(keys[i]); + } + }); +} + +void TitaevSortirovkaBetcheraSTL::BatcherStage(OutType &result, std::size_t array_size, std::size_t block, + std::size_t step) { + RunInParallel(array_size, [&result, block, step](std::size_t begin, std::size_t end) { + for (std::size_t i = begin; i < end; i++) { + const std::size_t partner = i ^ step; + if (partner <= i) { + continue; + } + const bool ascending = ((i & block) == 0); + const bool need_swap = ascending ? (result[i] > result[partner]) : (result[i] < result[partner]); + if (need_swap) { + std::swap(result[i], result[partner]); + } + } + }); +} + +void TitaevSortirovkaBetcheraSTL::BatcherSort() { + auto &result = GetOutput(); + const std::size_t n = result.size(); + if (n < 2) { + return; + } + for (std::size_t block = 2; block <= n; block <<= 1) { + for (std::size_t step = block >> 1; step > 0; step >>= 1) { + BatcherStage(result, n, block, step); + } + } +} + +bool TitaevSortirovkaBetcheraSTL::RunImpl() { + auto &input = GetInput(); + const std::size_t n = input.size(); + if (n <= 1) { + return true; + } + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + ConvertFromKeys(keys, GetOutput()); + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + return true; +} + +bool TitaevSortirovkaBetcheraSTL::PostProcessingImpl() { + return true; +} + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp b/tasks/titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp new file mode 100644 index 0000000000..ad2712e044 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + +#include "task/include/task.hpp" +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +class TitaevSortirovkaBetcheraTBB : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kTBB; + } + explicit TitaevSortirovkaBetcheraTBB(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + static void ConvertToKeys(const InType &input, std::vector &keys); + static void RadixSort(std::vector &keys); + static void ConvertFromKeys(const std::vector &keys, OutType &output); + void BatcherSort(); + static void BatcherStage(OutType &result, std::size_t array_size, std::size_t block, std::size_t step); +}; + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/tbb/report.md b/tasks/titaev_m_sortirovka_betchera/tbb/report.md new file mode 100644 index 0000000000..521ee858b1 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/tbb/report.md @@ -0,0 +1,145 @@ +# Поразрядная сортировка для вещественных чисел с чётно-нечётным слиянием Бэтчера - TBB + +- Student: Титаев М., group 3823Б1ФИ1 +- Technology: TBB +- Variant: 20 + +## 1. Introduction + +TBB-версия использует Intel oneAPI Threading Building Blocks для параллельного выполнения этапов +преобразования данных и сети чётно-нечётного слияния Бэтчера. Цель — оценить производительность +библиотеки задач TBB на данной постановке и сравнить её с baseline. + +## 2. Problem Statement + +Задача совпадает с остальными версиями: отсортировать массив `InType = std::vector` по +неубыванию, результат имеет тип `OutType = std::vector`. + +## 3. Baseline Algorithm (Sequential) + +Baseline описан в `seq/report.md`. Он выполняет преобразование `double` в упорядоченные 64-битные ключи, +поразрядную сортировку по основанию 256 и обратное преобразование, после чего при размере, равном степени +двойки, применяет сеть Бэтчера. + +## 4. Parallelization Scheme + +TBB-реализация распараллеливает следующие этапы: + +1. **Преобразование ключей:** прямое и обратное преобразование выполняется через +`oneapi::tbb::parallel_for` по `blocked_range`. +2. **Поразрядная сортировка:** проходы счётной сортировки выполняются последовательно, что сохраняет +стабильность распределения. +3. **Сеть Бэтчера:** каждый шаг сравнения распараллелен через `oneapi::tbb::parallel_for`; пары индексов +на одном шаге не пересекаются. + +Число потоков задаётся через `PPC_NUM_THREADS`. + +## 5. Implementation Details + +- Файлы: `tbb/include/ops_tbb.hpp`, `tbb/src/ops_tbb.cpp`. +- Класс: `TitaevSortirovkaBetcheraTBB`. +- Параллельные участки выражены через `parallel_for` и `blocked_range`. +- Проходы поразрядной сортировки оставлены последовательными ради стабильности. + +Память: один временный массив ключей и массив счётчиков на 256 корзин. + +## 6. Experimental Setup + +**Аппаратное обеспечение:** + +- **CPU:** 12th Gen Intel(R) Core(TM) i5-12450H (2.00 GHz, 8 ядер / 12 потоков) +- **RAM:** 16 ГБ +- **OS:** Windows 11 Pro x64 +- **MPI:** Microsoft MPI (MS-MPI) 10.1 + +**Инструменты:** + +- **Сборка:** CMake +- **Компилятор:** MSVC 19.x +- **Конфигурация:** Release + +**Окружение:** + +- **PPC_NUM_THREADS:** задаёт число потоков для OMP, TBB, STL и потоковой части ALL. +- **PPC_NUM_PROC / mpiexec -n:** задаёт число MPI-процессов для ALL. +- Для ALL конфигурация записывается в формате `ranks × threads`. + +**Генерация данных:** + +- Тесты генерируют входные данные автоматически. +- Для performance-теста используется массив из `2^20` элементов с убывающими значениями. +- Внешние файлы с данными не используются. + +## 7. Results and Discussion + +### 7.1 Correctness + +Корректность проверялась функциональными тестами из `tests/functional/main.cpp`. Выходной массив +проверяется на упорядоченность по неубыванию. Результаты TBB-версии совпадают с baseline на всех тестовых +наборах. + +### 7.2 Performance + +Используемые обозначения: + +```txt +time — время выполнения performance-теста; +speedup = time_seq / time_mode; +efficiency = speedup / workers; +workers — количество исполнителей: потоков для OMP/TBB/STL, ranks × threads для ALL. +``` + +| Mode | Count | Time, s | Speedup | Efficiency | +| --- | --- | --- | --- | --- | +| seq | 1 | 0.159970 | 1.00 | N/A | +| tbb | 2 | 0.123646 | 1.29 | 64.69% | +| tbb | 4 | 0.074043 | 2.16 | 54.01% | +| tbb | 8 | 0.063984 | 2.50 | 31.25% | + +Производительность TBB растёт с числом потоков и приближается к результату OMP при восьми потоках. +Накладные расходы на планирование задач TBB заметны на малом числе потоков, поэтому на двух потоках +ускорение невелико. + +## 8. Conclusions + +TBB-версия показывает рост производительности с увеличением числа потоков, наилучший результат получен на +восьми потоках. Эффективность снижается с ростом числа потоков из-за накладных расходов на планирование +задач и большого числа синхронизированных проходов в сети Бэтчера. + +## 9. References + +1. OpenMP Architecture Review Board. OpenMP Application Programming Interface. +2. oneAPI Threading Building Blocks Documentation. +3. Microsoft MPI Documentation. +4. ISO C++ Standard Library Documentation: `std::thread`. + +## Appendix (Optional) + +```cpp +void TitaevSortirovkaBetcheraTBB::BatcherSort() { + auto &result = GetOutput(); + const size_t n = result.size(); + if (n < 2) { + return; + } + + for (size_t k = 2; k <= n; k <<= 1) { + for (size_t j = k >> 1; j > 0; j >>= 1) { + oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0, n), + [&](const oneapi::tbb::blocked_range &r) { + for (size_t i = r.begin(); i < r.end(); i++) { + const size_t l = i ^ j; + if (l > i && l < n) { + const bool ascending = ((i & k) == 0); + const bool need_swap = + ascending ? (result[i] > result[l]) : (result[i] < result[l]); + if (need_swap) { + std::swap(result[i], result[l]); + } + } + } + }); + } + } +} +``` diff --git a/tasks/titaev_m_sortirovka_betchera/tbb/src/ops_tbb.cpp b/tasks/titaev_m_sortirovka_betchera/tbb/src/ops_tbb.cpp new file mode 100644 index 0000000000..5770ab9a42 --- /dev/null +++ b/tasks/titaev_m_sortirovka_betchera/tbb/src/ops_tbb.cpp @@ -0,0 +1,159 @@ +#include "titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp" + +#include +#include + +#include +#include +#include +#include + +#include "titaev_m_sortirovka_betchera/common/include/common.hpp" + +namespace titaev_m_sortirovka_betchera { + +namespace { + +uint64_t DoubleToOrderedUint(double value) { + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(double)); + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits = ~bits; + } else { + bits ^= kSignMask; + } + return bits; +} + +double OrderedUintToDouble(uint64_t bits) { + constexpr uint64_t kSignMask = (1ULL << 63); + if ((bits & kSignMask) != 0ULL) { + bits ^= kSignMask; + } else { + bits = ~bits; + } + double result = 0.0; + std::memcpy(&result, &bits, sizeof(double)); + return result; +} + +} // namespace + +TitaevSortirovkaBetcheraTBB::TitaevSortirovkaBetcheraTBB(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput().clear(); +} + +bool TitaevSortirovkaBetcheraTBB::ValidationImpl() { + return !GetInput().empty(); +} + +bool TitaevSortirovkaBetcheraTBB::PreProcessingImpl() { + GetOutput() = GetInput(); + return true; +} + +void TitaevSortirovkaBetcheraTBB::ConvertToKeys(const InType &input, std::vector &keys) { + const std::size_t n = input.size(); + oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0, n), + [&input, &keys](const oneapi::tbb::blocked_range &range) { + for (std::size_t i = range.begin(); i < range.end(); i++) { + keys[i] = DoubleToOrderedUint(input[i]); + } + }); +} + +void TitaevSortirovkaBetcheraTBB::RadixSort(std::vector &keys) { + const std::size_t n = keys.size(); + if (n <= 1) { + return; + } + + constexpr int kBits = 8; + constexpr int kBuckets = 1 << kBits; + constexpr int kPasses = 64 / kBits; + + std::vector tmp(n); + + for (int pass = 0; pass < kPasses; pass++) { + std::vector count(kBuckets, 0); + for (std::size_t i = 0; i < n; i++) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + count[bucket]++; + } + for (int i = 1; i < kBuckets; i++) { + count[i] += count[i - 1]; + } + for (std::size_t i = n; i-- > 0;) { + const std::size_t bucket = (keys[i] >> (pass * kBits)) & (kBuckets - 1); + tmp[--count[bucket]] = keys[i]; + } + keys.swap(tmp); + } +} + +void TitaevSortirovkaBetcheraTBB::ConvertFromKeys(const std::vector &keys, OutType &output) { + const std::size_t n = keys.size(); + output.resize(n); + oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0, n), + [&keys, &output](const oneapi::tbb::blocked_range &range) { + for (std::size_t i = range.begin(); i < range.end(); i++) { + output[i] = OrderedUintToDouble(keys[i]); + } + }); +} + +void TitaevSortirovkaBetcheraTBB::BatcherStage(OutType &result, std::size_t array_size, std::size_t block, + std::size_t step) { + oneapi::tbb::parallel_for(oneapi::tbb::blocked_range(0, array_size), + [&result, block, step](const oneapi::tbb::blocked_range &range) { + for (std::size_t i = range.begin(); i < range.end(); i++) { + const std::size_t partner = i ^ step; + if (partner <= i) { + continue; + } + const bool ascending = ((i & block) == 0); + const bool need_swap = ascending ? (result[i] > result[partner]) : (result[i] < result[partner]); + if (need_swap) { + std::swap(result[i], result[partner]); + } + } + }); +} + +void TitaevSortirovkaBetcheraTBB::BatcherSort() { + auto &result = GetOutput(); + const std::size_t n = result.size(); + if (n < 2) { + return; + } + for (std::size_t block = 2; block <= n; block <<= 1) { + for (std::size_t step = block >> 1; step > 0; step >>= 1) { + BatcherStage(result, n, block, step); + } + } +} + +bool TitaevSortirovkaBetcheraTBB::RunImpl() { + auto &input = GetInput(); + const std::size_t n = input.size(); + if (n <= 1) { + return true; + } + std::vector keys(n); + ConvertToKeys(input, keys); + RadixSort(keys); + ConvertFromKeys(keys, GetOutput()); + if ((n & (n - 1)) == 0) { + BatcherSort(); + } + return true; +} + +bool TitaevSortirovkaBetcheraTBB::PostProcessingImpl() { + return true; +} + +} // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/tests/functional/main.cpp b/tasks/titaev_m_sortirovka_betchera/tests/functional/main.cpp index 50e7e2be38..0e57acbf56 100644 --- a/tasks/titaev_m_sortirovka_betchera/tests/functional/main.cpp +++ b/tasks/titaev_m_sortirovka_betchera/tests/functional/main.cpp @@ -1,91 +1,72 @@ #include +#include +#include #include -#include -#include -#include #include #include -#include -#include "task/include/task.hpp" +#include "titaev_m_sortirovka_betchera/all/include/ops_all.hpp" #include "titaev_m_sortirovka_betchera/common/include/common.hpp" #include "titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp" #include "titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp" +#include "titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp" +#include "titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp" #include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" namespace titaev_m_sortirovka_betchera { -using TaskFactory = std::function>(InType)>; -using ParamType = std::tuple; - -class TitaevBatcherRadixFuncTests : public ppc::util::BaseRunFuncTests { +class TitaevSortirovkaBetcheraFuncTests : public ppc::util::BaseRunFuncTests { public: - static std::string PrintTestParam( - const testing::TestParamInfo &info) { - return std::get<1>(info.param); + static std::string PrintTestParam(const TestType &test_param) { + return std::to_string(std::get<0>(test_param)) + "_" + std::get<1>(test_param); } protected: - InType input; - void SetUp() override { - auto full_param = GetParam(); - TestType param = std::get<2>(full_param); - const int size = std::get<0>(param); - - std::random_device rd; - std::mt19937_64 gen(rd()); - std::uniform_real_distribution dist(-1000.0, 1000.0); - - input.resize(static_cast(size)); - for (auto &val : input) { - val = dist(gen); + TestType params = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + size_ = std::get<0>(params); + input_data_.resize(static_cast(size_)); + for (int i = 0; i < size_; i++) { + input_data_[i] = static_cast(size_ - i) - 0.5; } } - bool CheckTestOutputData(OutType &output) final { - if (output.size() != input.size()) { + bool CheckTestOutputData(OutType &output_data) final { + if (output_data.size() != static_cast(size_)) { return false; } - for (size_t i = 1; i < output.size(); i++) { - if (output[i] < output[i - 1]) { - return false; - } - } - return true; + return std::ranges::is_sorted(output_data); } InType GetTestInputData() final { - return input; + return input_data_; } + + private: + int size_ = 0; + InType input_data_; }; namespace { - -std::shared_ptr> CreateSeqTask(const InType &in) { - return std::make_shared(in); -} - -std::shared_ptr> CreateOmpTask(const InType &in) { - return std::make_shared(in); +TEST_P(TitaevSortirovkaBetcheraFuncTests, SortCorrectness) { + ExecuteTest(GetParam()); } -using ActualParamType = TitaevBatcherRadixFuncTests::ParamType; - -const std::vector kSeqParams = {{CreateSeqTask, "seq_size_128", TestType{128, "size_128"}}, - {CreateSeqTask, "seq_size_512", TestType{512, "size_512"}}, - {CreateSeqTask, "seq_size_1024", TestType{1024, "size_1024"}}}; - -const std::vector kOmpParams = {{CreateOmpTask, "omp_size_128", TestType{128, "size_128"}}, - {CreateOmpTask, "omp_size_512", TestType{512, "size_512"}}, - {CreateOmpTask, "omp_size_1024", TestType{1024, "size_1024"}}}; - -INSTANTIATE_TEST_SUITE_P(SequentialTests, TitaevBatcherRadixFuncTests, ::testing::ValuesIn(kSeqParams), - TitaevBatcherRadixFuncTests::PrintTestParam); +const std::array kTestParam = { + std::make_tuple(1, "N1"), std::make_tuple(2, "N2"), std::make_tuple(4, "N4"), std::make_tuple(8, "N8"), + std::make_tuple(16, "N16"), std::make_tuple(32, "N32"), std::make_tuple(7, "N7"), std::make_tuple(100, "N100")}; -INSTANTIATE_TEST_SUITE_P(OpenMPTests, TitaevBatcherRadixFuncTests, ::testing::ValuesIn(kOmpParams), - TitaevBatcherRadixFuncTests::PrintTestParam); +const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_titaev_m_sortirovka_betchera), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_titaev_m_sortirovka_betchera), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_titaev_m_sortirovka_betchera), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_titaev_m_sortirovka_betchera), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_titaev_m_sortirovka_betchera)); +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); +const auto kFuncTestName = TitaevSortirovkaBetcheraFuncTests::PrintFuncTestName; +INSTANTIATE_TEST_SUITE_P(SortFuncTests, TitaevSortirovkaBetcheraFuncTests, kGtestValues, kFuncTestName); } // namespace } // namespace titaev_m_sortirovka_betchera diff --git a/tasks/titaev_m_sortirovka_betchera/tests/performance/main.cpp b/tasks/titaev_m_sortirovka_betchera/tests/performance/main.cpp index ccca55be9a..256082188b 100644 --- a/tasks/titaev_m_sortirovka_betchera/tests/performance/main.cpp +++ b/tasks/titaev_m_sortirovka_betchera/tests/performance/main.cpp @@ -1,60 +1,59 @@ #include -#include -#include +#include +#include +#include "titaev_m_sortirovka_betchera/all/include/ops_all.hpp" #include "titaev_m_sortirovka_betchera/common/include/common.hpp" #include "titaev_m_sortirovka_betchera/omp/include/ops_omp.hpp" +#include "titaev_m_sortirovka_betchera/seq/include/ops_seq.hpp" +#include "titaev_m_sortirovka_betchera/stl/include/ops_stl.hpp" +#include "titaev_m_sortirovka_betchera/tbb/include/ops_tbb.hpp" #include "util/include/perf_test_util.hpp" namespace titaev_m_sortirovka_betchera { -class TitaevBatcherRadixPerfTests : public ppc::util::BaseRunPerfTests { +class TitaevSortirovkaBetcheraPerfTests : public ppc::util::BaseRunPerfTests { protected: - static constexpr size_t kSize = 1000000; - InType input; - void SetUp() override { - std::random_device rd; - std::mt19937_64 gen(rd()); - std::uniform_real_distribution dist(-100000.0, 100000.0); - - input.resize(kSize); - for (auto &val : input) { - val = dist(gen); + constexpr int kSize = 1 << 20; + input_data_.resize(kSize); + for (int i = 0; i < kSize; i++) { + input_data_[i] = static_cast(kSize - i) - 0.5; } } - bool CheckTestOutputData(OutType &output) final { - if (output.size() != input.size()) { - return false; - } - for (size_t i = 1; i < output.size(); i++) { - if (output[i] < output[i - 1]) { - return false; - } - } - return true; + bool CheckTestOutputData(OutType &output_data) final { + return std::ranges::is_sorted(output_data); } InType GetTestInputData() final { - return input; + return input_data_; } + + private: + InType input_data_; }; -TEST_P(TitaevBatcherRadixPerfTests, RunPerformanceOMP) { +TEST_P(TitaevSortirovkaBetcheraPerfTests, RunPerfModes) { ExecuteTest(GetParam()); } namespace { - -const auto kPerfTasks = +const auto kSeqPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_titaev_m_sortirovka_betchera); +const auto kOmpPerfTasks = ppc::util::MakeAllPerfTasks(PPC_SETTINGS_titaev_m_sortirovka_betchera); - -const auto kValues = ppc::util::TupleToGTestValues(kPerfTasks); -const auto kNameGen = TitaevBatcherRadixPerfTests::CustomPerfTestName; - -INSTANTIATE_TEST_SUITE_P(PerformanceSortingTests, TitaevBatcherRadixPerfTests, kValues, kNameGen); - +const auto kTbbPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_titaev_m_sortirovka_betchera); +const auto kStlPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_titaev_m_sortirovka_betchera); +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks(PPC_SETTINGS_titaev_m_sortirovka_betchera); + +const auto kPerfTasks = std::tuple_cat(kSeqPerfTasks, kOmpPerfTasks, kTbbPerfTasks, kStlPerfTasks, kAllPerfTasks); +const auto kGtestValues = ppc::util::TupleToGTestValues(kPerfTasks); +const auto kPerfTestName = TitaevSortirovkaBetcheraPerfTests::CustomPerfTestName; +INSTANTIATE_TEST_SUITE_P(RunModeTests, TitaevSortirovkaBetcheraPerfTests, kGtestValues, kPerfTestName); } // namespace } // namespace titaev_m_sortirovka_betchera