diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/all/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/all/report.md new file mode 100644 index 0000000000..c7a8fa703f --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/all/report.md @@ -0,0 +1,265 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием — ALL + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Technology: ALL +- Variant: 19 + +## 1. Контекст + +В данной задаче последовательная реализация поразрядной сортировки для вещественных +чисел типа `double` переносится на гибридную версию ALL, которая объединяет два уровня параллелизма: + +- Межпроцессный — MPI; +- Внутрипроцессный — OpenMP. + +Такая схема позволяет использовать MPI для распределения участков массива между `rank`-ами, +а OpenMP — для параллельной обработки локальных данных внутри каждого процесса. + +Сохраняется вычислительная часть, а именно: преобразование `double` в `uint64_t`, +поразрядная сортировка `RadixSortUInt`, обратное преобразование `uint64_t` в `double`. + +## 2. Постановка задачи + +Необходимо выполнить сортировку массива вещественных чисел типа `double` +по возрастанию с использованием гибрида MPI + OpenMP. + +Постановка задачи ALL совпадает с постановкой базовой задачи (SEQ), которая находится +в файле `seq/report.md`. + +### Входные данные + +Целое число `N`, определяющее размер генерируемого массива (`N > 0`): + +```hpp +using InType = int; +``` + +### Выходные данные + +Признак успешной сортировки: + +- `1` — массив отсортирован корректно; +- `0` — обнаружена ошибка сортировки или потеря данных. + +```hpp +using OutType = int; +``` + +## 3. Базовый алгоритм + +Алгоритм (SEQ) состоит из нескольких этапов: + +1. Генерация массива случайных вещественных чисел (`PreProcessingImpl`); +2. Разделение массива на две части; +3. Преобразование чисел `double` в сортируемое представление `uint64_t` (`DoubleToSortable`); +4. Поразрядная сортировка (`RadixSortUInt`); +5. Обратное преобразование в `double` (`SortableToDouble`); +6. Слияние отсортированных частей (`MergeSorted`); +7. Проверка корректности результата. + +## 4. Межпроцессная схема + +### Роли `rank`-ов + +**`rank 0`:** + +- Генерирует исходный массив; +- Распределяет данные между процессами; +- Собирает локальные результаты; +- Выполняет финальное слияние; +- Проверяет корректность результата. + +**`rank > 0`:** + +- Получают собственный диапазон данных; +- Выполняют локальную обработку; +- Отправляют отсортированный сегмент обратно. + +### Распределение данных + +Размеры локальных частей вычисляются функцией `SplitMpiData`: + +```cpp +SplitMpiData(total_elements, size, elem_count, start_index); +``` + +Для каждого процесса вычисляются: + +- `elem_count[i]` — число элементов; +- `start_index[i]` — начало диапазона. + +### MPI-вызовы + +| Вызов | Назначение | +| -------------- | ----------------------------------------------------| +| `MPI_Bcast` | Передача размера массива всем процессам | +| `MPI_Scatterv` | Распределение частей массива между `rank`-ами | +| `MPI_Gatherv` | Сбор локально отсортированных частей на `rank 0` | +| `MPI_Barrier` | Синхронизация процессов после завершения вычислений | + +## 5. Внутрипроцессная схема + +Внутри каждого MPI-процесса используется OpenMP: + +```cpp +#pragma omp parallel +{ +#pragma omp for + for (int i = 0; i < my_elem_count; i++) + local_bits[i] = DoubleToSortable(local_array[i]); +} +RadixSortUInt(local_bits); +#pragma omp parallel +{ +#pragma omp for + for (int i = 0; i < my_elem_count; i++) + local_array[i] = SortableToDouble(local_bits[i]); +} +``` + +Поразрядная сортировка `RadixSortUInt` выполняется независимо внутри каждого MPI-процесса. +После завершения локальной обработки результаты собираются +на `rank 0` через `MPI_Gatherv`. +Финальное объединение сегментов выполняется функцией `MergeChunks`, +которая последовательно вызывает `MergeSorted`. + +## 6. Детали реализации + +**Файлы:** `all/include/ops_all.hpp`, `all/src/ops_all.cpp` + +| Метод | Назначение | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `ValidationImpl` | Проверяет, что количество элементов сортировки строго больше нуля: `GetInput() > 0` | +| `PreProcessingImpl` | Создает `array_` размера `N` на `rank 0`, заполняет случайными числами с плавающей точкой в диапазоне `[-100, 100]` | +| `RunImpl` | Выполняет MPI-распределение, OpenMP-обработку и сбор результатов | +| `PostProcessingImpl` | Проверяет корректность сортировки и синхронизирует результат между rank-ами | + +**Поля класса:** + +- `array_` — исходный массив; +- `result_` — результат после слияния. + +```cpp +MPI_Bcast(&total_elements, 1, MPI_INT, 0, MPI_COMM_WORLD); // Broadcast размера массива +MPI_Scatterv(...) // Распределение данных +MPI_Gatherv(...) // Сбор результатов +MPI_Barrier(MPI_COMM_WORLD); // Финальная синхронизация +``` + +**Потенциальные узкие места:** +Основными ограничениями производительности являются: + +- Последовательное слияние результатов на `rank 0`; +- Затраты на `MPI_Scatterv` и `MPI_Gatherv`; +- Синхронизация MPI-процессов; +- Конкуренция потоков OpenMP за память; +- Ограничение пропускной способности памяти при большом числе потоков. + +## 7. Проверка корректности + +Используются те же функциональные тесты, что для SEQ, OMP, TBB и STL (`tests/functional/main.cpp`, `N` от 2 до 5000). + +Проверки в `PostProcessingImpl` идентичны SEQ: `IsSorted` + `SameData`, ожидается `GetOutput() == 1`. + +При одинаковом `N` результат должен быть эквивалентен по критерию корректности (упорядоченность и сохранность данных). + +**Согласованность между rank-ами:** после `MPI_Bcast` все rank-ы получают одинаковый `GetOutput()`. + +## 8. Экспериментальная среда + +Hardware/OS: + +- процессор: Intel Core i7; +- ядра/потоки: 10 / 16; +- оперативная память: 16 GB; +- операционная система: Windows 11; +- архитектура: x64. + +Toolchain: + +- компилятор: Microsoft Visual C++ (MSVC); +- версия: 19.50.35725.0; +- тип сборки: Release; +- MPI: MS-MPI (`mpiexec`); +- система сборки: CMake. + +**Конфигурация** задаётся парой: + +- `mpiexec -n P` — число MPI-процессов (`P`); +- `OMP_NUM_THREADS=T` — потоков OpenMP на процесс; +- **total workers** = `P × T`. + +**Сборка:** + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +**Функциональные тесты:** + +```bash +mpiexec -n P ./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_all*" +``` + +**Тесты производительности:** + +```powershell +$env:OMP_NUM_THREADS="T" +mpiexec -n P ./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_all*" +``` + +## 9. Результаты + +Замер производительности проводился при размере входных данных `k_count = 1000000`. + +| Mode | Ranks | OMP | Workers | Time, s | Speedup | Efficiency | +| ---------- | ----- | --- | ------- | ---------- | ------- | ---------- | +| seq (task) | 1 | 1 | 1 | 0.08397980 | 1.000 | 100.00% | +| seq (pipe) | 1 | 1 | 1 | 0.12079842 | 1.000 | 100.00% | +| all (task) | 2 | 2 | 4 | 0.03612031 | 2.325 | 58.13% | +| all (pipe) | 2 | 2 | 4 | 0.05313855 | 2.273 | 56.83% | +| all (task) | 4 | 2 | 8 | 0.03854337 | 2.179 | 27.24% | +| all (pipe) | 4 | 2 | 8 | 0.05073954 | 2.381 | 29.76% | +| all (task) | 4 | 4 | 16 | 0.03789758 | 2.216 | 13.85% | +| all (pipe) | 4 | 4 | 16 | 0.05192675 | 2.326 | 14.54% | +| all (task) | 8 | 2 | 16 | 0.04100889 | 2.048 | 12.80% | +| all (pipe) | 8 | 2 | 16 | 0.05182306 | 2.331 | 14.57% | + +Использование гибридной схемы MPI + OpenMP позволило получить ускорение по сравнению с последовательной реализацией. + +Наилучшие результаты были получены при умеренном количестве процессов и потоков (2х2 и 4х2). +При дальнейшем увеличении общего числа workers ускорение растет незначительно, а эффективность начинает снижаться. + +Снижение эффективности связано с дополнительными затратами +на межпроцессное взаимодействие и синхронизацию: + +- Передача данных между процессами через `MPI_Scatterv` и `MPI_Gatherv`; +- Синхронизация процессов через `MPI_Bcast` и `MPI_Barrier`; +- Последовательное слияние локальных результатов на `rank 0`; +- Рост конкуренции за пропускную способность памяти; +- Накладные расходы на управление OpenMP-потоками внутри каждого MPI-процесса. + +## 10. Выводы + +Гибридная реализация MPI + OpenMP позволила существенно сократить время выполнения по сравнению +с последовательной версией алгоритма (более чем в 2 раза). + +MPI используется для распределения данных между процессами, +а OpenMP — для ускорения локальной обработки данных внутри каждого процесса. + +Гибридная схема наиболее оправдана при больших объемах данных, +когда вычислительная нагрузка компенсирует стоимость +MPI-коммуникаций и синхронизации. + +При слишком большом числе workers эффективность начинает снижаться +из-за накладных расходов на: + +- Передачу данных между процессами; +- Синхронизацию MPI и OpenMP; +- Последовательное слияние результатов на `rank 0`; +- Конкуренцию за память и кэш. + +Для небольших задач использование большого числа процессов и потоков +может быть неэффективным, поскольку накладные расходы +перевешивают выигрыш от параллельной обработки. diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/omp/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/omp/report.md new file mode 100644 index 0000000000..0c86776319 --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/omp/report.md @@ -0,0 +1,235 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием — OMP + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Technology: OMP +- Variant: 19 + +## 1. Контекст + +В данной задаче последовательная реализация поразрядной сортировки +для вещественных чисел типа `double` переносится на технологию OpenMP. +Сохраняется вычислительная часть, а именно: преобразование `double` в `uint64_t`, поразрядная +сортировка `RadixSortUInt`, обратное преобразование `uint64_t` в `double`. + +В отличие от последовательной версии, в OpenMP-реализации массив делится на количество сегментов, +равное числу потоков (`omp_get_max_threads()`, задаётся через `OMP_NUM_THREADS`). Каждый сегмент +сортируется независимо в своём потоке. + +После выхода из параллельной области происходит последовательное слияние отсортированных сегментов. + +## 2. Постановка задачи + +Необходимо выполнить сортировку массива вещественных чисел типа `double` +по возрастанию с использованием OpenMP. + +Постановка задачи OMP совпадает с постановкой базовой задачи (SEQ), которая находится +в файле `seq/report.md`. + +### Входные данные + +Целое число `N`, определяющее размер генерируемого массива (`N > 0`): + +```hpp +using InType = int; +``` + +### Выходные данные + +Признак успешной сортировки: + +- `1` — массив отсортирован корректно; +- `0` — обнаружена ошибка сортировки или потеря данных. + +```hpp +using OutType = int; +``` + +## 3. Базовый алгоритм + +Алгоритм (SEQ) состоит из нескольких этапов: + +1. Генерация массива случайных вещественных чисел (`PreProcessingImpl`); +2. Разделение массива на две части; +3. Преобразование чисел `double` в сортируемое представление `uint64_t` (`DoubleToSortable`); +4. Поразрядная сортировка (`RadixSortUInt`); +5. Обратное преобразование в `double` (`SortableToDouble`); +6. Слияние отсортированных частей (`MergeSorted`); +7. Проверка корректности результата. + +## 4. Схема распараллеливания + +### Параллелизуемая область + +Распараллеливание выполняется на этапе локальной сортировки частей массива. Исходный +массив делится между потоками на диапазоны. Каждый поток: + +1. Вычисляет собственные индексные границы (`left_idx`, `right_idx`); +2. Преобразует элементы `double` в `uint64_t`; +3. Выполняет локальную поразрядную сортировку; +4. Выполняет обратное преобразование; +5. Сохраняет результат в собственный элемент `local_results`. + +```cpp +int n_threads = omp_get_max_threads(); +// ... +#pragma omp parallel num_threads(n_threads) default(none) \ + shared(n, n_threads, ref_array, ref_local_results) +{ + int thread_id = omp_get_thread_num(); + int left_idx = (thread_id * n) / n_threads; + int right_idx = ((thread_id + 1) * n) / n_threads; +} +``` + +### Shared / private переменные + +**shared:** + +- n — общий размер массива; +- n_threads — число потоков; +- ref_array — исходный массив элементов; +- ref_local_results — локальные результаты потоков. + +**private:** + +- thread_id — номер потока; +- left_idx — граница сегмента; +- right_idx — граница сегмента; +- local_size — локальный размер; +- local_bits — локальные значения. + +### Reduction / atomic / critical + +Использование critical, atomic и reduction не требуется, поскольку каждый поток работает +только со своим диапазоном данных и пишет в свой `local_results[thread_id]`. + +### Schedule + +Распределение работы выполняется вручную на основе уникального индекса потока. + +В конце параллельной области существует неявный барьер OpenMP, гарантирующий завершение +локальной сортировки во всех потоках перед началом слияния. + +## 5. Детали реализации + +**Файлы:** `omp/include/ops_omp.hpp`, `omp/src/ops_omp.cpp` + +| Метод | Назначение | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------| +| `ValidationImpl` | Проверяет, что количество элементов сортировки строго больше нуля: `GetInput() > 0` | +| `PreProcessingImpl` | Создает `array_` размера `N`, заполняет случайными числами с плавающей точкой в диапазоне `[-100, 100]` | +| `RunImpl` | Организует параллельную область OpenMP, перевод в `uint64_t`, `RadixSortUInt` для `n_threads` сегментов, обратное преобразование и `MergeSorted` | +| `PostProcessingImpl` | Проверяет сортировку (`IsSorted`) и полноту данных (`SameData`), записывает результат в `GetOutput()` (1 — успех, 0 — ошибка) | + +**Поля класса:** + +- `array_` — исходный массив; +- `result_` — результат после слияния. + +Относительно последовательной версии (SEQ) есть следующие изменения в методе `RunImpl()`: + +1. Запуск сортировки двух половин заменен на параллельную область `#pragma omp parallel default(none) ...`; +2. Для хранения локального результата на каждом потоке создан вектор векторов `local_results`; +3. Внутри параллельного блока каждый рабочий поток выполняет локальное копирование своего + участка памяти, преобразование `DoubleToSortable`, вызов `RadixSortUInt` и обратное восстановление типов; +4. После закрытия параллельного блока осуществляется последовательное слияние промежуточных + векторов из `local_results` в единый вектор `result_` с помощью функции `MergeSorted`. + +```cpp +int n_threads = omp_get_max_threads(); +std::vector> local_results(n_threads); + +#pragma omp parallel num_threads(n_threads) default(none) shared(n, n_threads, ref_array, ref_local_results) +{ + int thread_id = omp_get_thread_num(); + int left_idx = (thread_id * n) / n_threads; + int right_idx = ((thread_id + 1) * n) / n_threads; + if (left_idx < right_idx) { + ref_local_results[thread_id] = ...; + } +} +result_ = local_results[0]; +for (int i = 1; i < n_threads; ++i) + if (!local_results[i].empty()) + result_ = MergeSorted(result_, local_results[i]); +``` + +Риск возникновения гонки данных предотвращается за счет того, что каждый поток записывает результат +в собственный элемент `local_results[thread_id]`. + +## 6. Проверка корректности + +Используются те же тесты, что и для SEQ (`tests/functional/main.cpp`, `N` от 2 до 5000). + +Проверки в `PostProcessingImpl` идентичны SEQ: `IsSorted` + `SameData`, ожидается `GetOutput() == 1`. + +## 7. Экспериментальная среда + +Hardware/OS: + +- процессор: Intel Core i7; +- ядра/потоки: 10 / 16; +- оперативная память: 16 GB; +- операционная система: Windows 11; +- архитектура: x64. + +Toolchain: + +- компилятор: Microsoft Visual C++ (MSVC); +- версия: 19.50.35725.0; +- тип сборки: Release; +- система сборки: CMake. + +Число потоков задается переменной окружения `OMP_NUM_THREADS` = {1, 2, 4, 8}. + +**Команды запуска:** + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +**Функциональные тесты:** + +```powershell +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_omp*" +``` + +**Тесты производительности:** + +```powershell +$env:OMP_NUM_THREADS="T" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_omp*" +``` + +где `T` — количество потоков. + +## 8. Результаты + +Замер производительности проводился при размере входных данных `k_count = 1000000`. + +| Mode | Count | Time, s | Speedup | Efficiency | +| -----------| ----- | ---------- | ------- | ---------- | +| seq (task) | 1 | 0.08397980 | 1.000 | 100.00% | +| seq (pipe) | 1 | 0.12079842 | 1.000 | 100.00% | +| omp (task) | 2 | 0.03507492 | 2.394 | 119.70% | +| omp (pipe) | 2 | 0.05482724 | 2.203 | 110.15% | +| omp (task) | 4 | 0.03516550 | 2.388 | 59.70% | +| omp (pipe) | 4 | 0.04544188 | 2.658 | 66.45% | +| omp (task) | 8 | 0.03286826 | 2.555 | 31.94% | +| omp (pipe) | 8 | 0.04531648 | 2.666 | 33.33% | + +Использование OpenMP позволило получить ускорение по сравнению с последовательной реализацией. Наилучший +результат достигнут при 2 потоках. При дальнейшем увеличении числа потоков ускорение почти не изменяется, +а эффективность снижается. + +Одним из основных узких мест является последовательное слияние финальных блоков, выполняемое одним потоком. +Дополнительно производительность ограничивается пропускной способностью памяти при обработке больших массивов данных. + +## 9. Выводы + +Технология OpenMP позволила существенно сократить время выполнения по сравнению с последовательной +реализацией (более чем в 2 раза). Наибольший выигрыш достигается при использовании 2 потоков. + +Дальнейшее увеличение числа потоков не приводит к пропорциональному росту производительности из-за ограниченной +масштабируемости алгоритма, последовательного этапа слияния и ограничений пропускной способности памяти. diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/report.md new file mode 100644 index 0000000000..0e74288e42 --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/report.md @@ -0,0 +1,290 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Variant: 19 +- Local reports: `seq/report.md`, `omp/report.md`, `tbb/report.md`, `stl/report.md`, `all/report.md` + +## 1. Введение + +Реализована задача поразрядной сортировки массива вещественных чисел +типа `double` с использованием простого последовательного слияния отсортированных частей. + +Задача подходит для сравнения разных моделей параллелизма, потому что: + +- Содержит независимые сегменты массива, которые можно распределять между потоками и процессами; +- Содержит вычислительно затратную сортировку; +- Содержит этапы синхронизации и объединения результатов; +- Имеет последовательное узкое место (слияние), ограничивающее масштабируемость; +- Имеет выраженную зависимость производительности от способа распределения данных. + +Реализованы пять backend-ов: + +| Backend | Модель | Кратко | +| ------- | ------------------- | ---------------------------------------------------------------------------| +| SEQ | последовательная | Базовая последовательная реализация | +| OMP | OpenMP | Параллельная обработка сегментов через `#pragma omp parallel` | +| TBB | Intel oneTBB | Распараллеливание через `tbb::parallel_for` | +| STL | `std::thread` | Ручное управление потоками и локальной сортировкой | +| ALL | гибрид MPI + OpenMP | MPI-распределение данных между `rank`-ами + OpenMP внутри каждого `rank`-а | + +## 2. Единая постановка задачи + +Цель — упорядочить по возрастанию массив случайных вещественных чисел типа `double`. + +Отличия backend-ов — в способе разбиения и синхронизации. + +### Вход / выход + +```hpp +using InType = int; // N > 0 — размер массива +using OutType = int; // 1 — успех, 0 — ошибка +``` + +### Ограничения + +- `N` строго положительно (`ValidationImpl`); +- Элементы генерируются в диапазоне `[-100.0, 100.0]`; +- Для radix-sort значения `double` преобразуются в `uint64_t` с сохранением порядка (`DoubleToSortable` / `SortableToDouble`). + +### Критерий корректности + +- Массив `result_` отсортирован по возрастанию (`IsSorted`); +- Сохранен набор исходных элементов — проверка XOR по 64-битным представлениям (`SameData`). + +## 3. Единая методика эксперимента + +### Окружение + +| Параметр | Значение | +| ---------------- | ------------------ | +| CPU | Intel Core i7 | +| Ядра / потоки ОС | 10 / 16 | +| RAM | 16 GB | +| ОС | Windows 11, x64 | +| Компилятор | MSVC 19.50.35725.0 | +| Сборка | Release, CMake | +| MPI (для ALL) | MS-MPI (`mpiexec`) | + +### Данные + +- Размер в perf-тестах: `k_count = 1 000 000` (`tests/performance/main.cpp`); +- Массив заполняется псевдослучайными `double` в `PreProcessingImpl` на `rank 0` (ALL); +- Для остальных backend-ов — в единственном процессе. + +### Переменные окружения + +| Backend | Управление параллелизмом | +| ------- | ---------------------------------- | +| SEQ | — | +| OMP | `OMP_NUM_THREADS` | +| TBB, STL| `PPC_NUM_THREADS` | +| ALL | `mpiexec -n P` + `OMP_NUM_THREADS` | + +### Метрики + +Speedup (ускорение): +`Speedup = T_seq / T_parallel` +где `T_seq` — время выполнения последовательной версии, `T_parallel` — время выполнения параллельной версии. + +Efficiency (эффективность): +`Efficiency = (Speedup / Workers) × 100%` +где `Workers` — общее число вычислительных исполнителей (потоков для OMP/TBB/STL +и произведение `Ranks × OMP threads` для ALL). + +### Повторы и агрегация + +Каждый тест запускался 7 раз, после чего вычислялось среднее время выполнения. + +Сравнение между backend-ами выполнялось при одинаковом `k_count` и одной машине. + +## 4. Сводка корректности + +### Сравнение с SEQ + +Все backend-ы используют одинаковые `IsSorted` и `SameData` (или эквивалент на rank 0 для ALL). +Ожидаемый `GetOutput() == 1` при корректной сортировке. + +### Функциональные тесты + +Файл: `tests/functional/main.cpp` + +| N | Имя теста | Описание | +| ---- | ---------------------- | ------------------------------------ | +| 2 | `2` | Сортировка массива из 2 элементов | +| 3 | `3` | Сортировка массива из 3 элементов | +| 4 | `4` | Сортировка массива из 4 элементов | +| 5 | `5` | Сортировка массива из 5 элементов | +| 10 | `10_1`, `10_2`, `10_3` | Сортировка массива из 10 элементов | +| 17 | `17` | Сортировка массива из 17 элементов | +| 50 | `50` | Сортировка массива из 50 элементов | +| 100 | `100` | Сортировка массива из 100 элементов | +| 1000 | `1000` | Сортировка массива из 1000 элементов | +| 5000 | `5000` | Сортировка массива из 5000 элементов | + +## 5. Агрегированные результаты + +Замер при `k_count = 1 000 000`. + +### 5.1. Режим task_run + +| Backend | Workers | Time, s | Speedup | Efficiency | +| -------------- | ---------- | ---------- | ------- | ---------- | +| seq | 1 | 0.08397980 | 1.000 | 100.00% | +| stl | 2 | 0.03192456 | 2.634 | 131.68% | +| omp | 8 | 0.03286826 | 2.555 | 31.94% | +| tbb | 4 | 0.03377934 | 2.486 | 62.15% | +| all | 4 (2×2) | 0.03612031 | 2.325 | 58.13% | +| omp | 2 | 0.03507492 | 2.394 | 119.70% | +| tbb | 2 | 0.03794540 | 2.213 | 110.65% | +| stl | 4 | 0.03874158 | 2.168 | 54.19% | +| all | 8 (4×2) | 0.03854337 | 2.179 | 27.24% | +| all | 16 (4×4) | 0.03789758 | 2.216 | 13.85% | +| stl | 8 | 0.04166961 | 2.015 | 25.19% | +| tbb | 8 | 0.03927820 | 2.138 | 26.73% | +| all | 16 (8×2) | 0.04100889 | 2.048 | 12.80% | + +### 5.2. Режим pipeline + +| Backend | Workers | Time, s | Speedup | Efficiency | +| ------------- | ------- | ---------- | ------- | ---------- | +| seq | 1 | 0.12079842 | 1.000 | 100.00% | +| omp | 4 | 0.04544188 | 2.658 | 66.45% | +| omp | 8 | 0.04531648 | 2.666 | 33.33% | +| stl | 2 | 0.04675974 | 2.583 | 129.15% | +| tbb | 2 | 0.04815750 | 2.508 | 125.42% | +| all | 8 (4×2) | 0.05073954 | 2.381 | 29.76% | +| tbb | 4 | 0.04839584 | 2.496 | 62.40% | +| all | 4 (2×2) | 0.05313855 | 2.273 | 56.83% | + +В ряде конфигураций наблюдается суперлинейное ускорение (`Efficiency > 100%`). +Это может быть связано с более эффективным использованием кэш-памяти +при разбиении массива между потоками и улучшением локальности данных по сравнению с последовательной версией. + +## 6. Интерпретация различий + +### SEQ (baseline) + +Делит массив на две половины, сортирует, сливает один раз. Нет накладных расходов на потоки/MPI. +Используется как эталон корректности и базового времени выполнения. + +### OMP + +**Сильные стороны:** обеспечивает простое распараллеливание циклов +с небольшими накладными расходами. + +**Слабые стороны:** + +- Ограниченная гибкость; +- Конкуренция потоков за память; +- Снижение эффективности при большом числе потоков. + +### TBB + +**Сильные стороны:** показывает высокие результаты за счет более гибкого планировщика и автоматического балансирования нагрузки. + +**Слабые стороны:** при 8 потоках медленнее, чем при 4. + +### STL + +**Сильные стороны:** позволяет полностью контролировать потоки. + +**Слабые стороны:** накладные расходы возрастают при увеличении числа потоков. + +### ALL (MPI + OMP) + +**Сильные стороны:** демонстрирует двухуровневый параллелизм: + +- MPI распределяет данные между процессами; +- OpenMP ускоряет локальную обработку. + +**Слабые стороны:** + +- Стоимость `MPI_Scatterv` и `MPI_Gatherv`; +- Синхронизация процессов; +- Последовательное объединение результатов; +- Снижение масштабируемости при большом числе workers. + +## 7. Репродуцируемость + +### Команды сборки + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +### Команды запуска функциональных тестов + +```powershell +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_seq*" +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_omp*" +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_tbb*" +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_stl*" +mpiexec -n 2 ./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_all*" +``` + +### Команды запуска тестов производительности + +```powershell +# SEQ +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_seq*" + +# OMP +$env:OMP_NUM_THREADS="8" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_omp*" + +# TBB / STL +$env:PPC_NUM_THREADS="4" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_tbb*" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_stl*" + +# ALL +$env:OMP_NUM_THREADS="2" +mpiexec -n 2 ./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_all*" +``` + +## 8. Заключение + +В рамках данной работы успешно реализованы и протестированы пять вариантов поразрядной +сортировки для вещественных чисел с простым слиянием. + +Все реализации корректны и проходят функциональные тесты. Анализ производительности показал, +что распараллеливание позволяет сократить время выполнения по сравнению с последовательной реализацией. +Наиболее стабильные и высокие результаты показали реализации на OpenMP и oneTBB. + +### Основные выводы по backend-ам + +- **SEQ** используется как базовая реализация для сравнения производительности и оценки ускорения. + +- **OMP** продемонстрировал хорошее ускорение при небольших накладных расходах. + Наиболее эффективными оказались конфигурации с 2–4 потоками. При дальнейшем увеличении числа + потоков эффективность снижается из-за конкуренции за память и ограничений пропускной способности системы. + +- **TBB** показал одни из наиболее стабильных результатов среди многопоточных backend-ов. + Планировщик oneTBB эффективно распределяет нагрузку + между потоками и уменьшает стоимость ручного управления параллелизмом. + +- **STL** (`std::thread`) обеспечил сопоставимое ускорение, однако требует ручного управления потоками, + синхронизацией и распределением диапазонов. При большом числе потоков накладные + расходы начинают ограничивать масштабируемость. + +- **ALL** (MPI + OpenMP) позволил реализовать двухуровневый параллелизм: распределение данных + между процессами и локальную многопоточную обработку внутри каждого `rank`-а. Гибридная схема + обеспечивает ускорение относительно SEQ, однако эффективность снижается при росте числа workers + из-за стоимости MPI-коммуникаций (`MPI_Scatterv`, `MPI_Gatherv`) и синхронизации процессов. + +Задача поразрядной сортировки демонстрирует +различия между моделями параллелизма и влияние накладных расходов +на масштабируемость вычислений. + +Полученные результаты подтверждают, что выбор технологии зависит не только от числа потоков, +но и от характера синхронизации и стоимости коммуникаций. + +## 9. Источники + +- Сысоев А. В., курс «Параллельное программирование для систем с общей памятью»; +- Документация курса ppc-2026-threads и структура примера `tasks/example_threads`; +- [oneAPI Threading Building Blocks. Documentation](https://uxlfoundation.github.io/oneTBB/); +- [MPI Standard Documentation](https://www.mpi-forum.org/docs/); +- [OpenMP API Specification](https://www.openmp.org/specifications/); +- [cppreference.com](https://en.cppreference.com/). diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/seq/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/seq/report.md new file mode 100644 index 0000000000..e45bf62be4 --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/seq/report.md @@ -0,0 +1,245 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием — SEQ + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Technology: SEQ +- Variant: 19 + +## 1. Контекст + +В задаче реализуется поразрядная сортировка (`Radix Sort`) для вещественных чисел типа `double` +с последующим простым слиянием двух отсортированных частей массива. +Последовательная реализация используется как эталонная версия для дальнейшего сравнения +с другими технологиями (OMP, TBB, STL, ALL). + +Алгоритм создан для обработки массива вещественных чисел с положительными и отрицательными значениями. +Для корректной работы поразрядной сортировки значения `double` предварительно преобразуются +в специальное представление `uint64_t`, сохраняющее порядок чисел при побайтной сортировке. + +## 2. Постановка задачи + +Основная цель задачи — упорядочить по возрастанию массив случайных вещественных чисел типа `double`. + +### Входные данные + +Целое число `N`, определяющее размер генерируемого массива (`N > 0`): + +```hpp +using InType = int; +``` + +### Выходные данные + +Признак успешной сортировки: + +- `1` — массив отсортирован корректно; +- `0` — обнаружена ошибка сортировки или потеря данных. + +```hpp +using OutType = int; +``` + +### Ограничения + +- Размер массива должен быть строго больше нуля; +- Значения элементов по умолчанию распределены в диапазоне от `-100.0` до `100.0`. + +### Крайние случаи + +- `N <= 0` — входные данные считаются некорректными (`ValidationImpl` возвращает `false`); +- Пустой массив не допускается к выполнению алгоритма; +- Проверяется сохранение всех исходных данных после сортировки. + +## 3. Базовый алгоритм + +### Пошаговое описание + +Алгоритм (SEQ) состоит из нескольких этапов: + +1. Генерация массива случайных вещественных чисел (`PreProcessingImpl`); +2. Разделение массива на две части; +3. Преобразование чисел `double` в сортируемое представление `uint64_t` (`DoubleToSortable`); +4. Поразрядная сортировка каждой части массива (`RadixSortUInt`); +5. Обратное преобразование в `double` (`SortableToDouble`); +6. Простое слияние двух отсортированных частей (`MergeSorted`); +7. Проверка корректности результата. + +### Временная асимптотика + +Временная сложность: `O(8⋅N) ≈ O(N)` + +### Пространственная асимптотика + +Пространственная сложность: `O(N)` + +### Критерий корректности + +После завершения алгоритма: + +- Массив должен быть упорядочен по возрастанию; +- Все исходные элементы должны сохраниться. + +## 4. Детали реализации + +**Файлы:** `seq/include/ops_seq.hpp`, `seq/src/ops_seq.cpp` + +| Метод | Назначение | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ValidationImpl` | Проверяет, что количество элементов сортировки строго больше нуля: `GetInput() > 0` | +| `PreProcessingImpl` | Создает `array_` размера `N`, заполняет случайными числами с плавающей точкой в диапазоне `[-100, 100]` | +| `RunImpl` | Выполняет перевод элементов в `uint64_t`, поразрядную сортировку `RadixSortUInt` двух половин, возвращает данные в формат `double` и выполняет `MergeSorted` | +| `PostProcessingImpl` | Проверяет сортировку (`IsSorted`) и полноту данных (`SameData`), результат этой проверки записывает в `GetOutput()` (если обе проверки успешны = 1, иначе = 0) | + +**Поля класса:** + +- `array_` — исходный массив; +- `result_` — результат после слияния. + +**Вспомогательные функции**: + +- `DoubleToSortable` / `SortableToDouble` — преобразование из/в `double` в/из `uint64_t` с сохранением порядка; +- `RadixSortUInt` — локальная поразрядная сортировка; +- `MergeSorted` — слияние двух векторов; +- `IsSorted`, `SameData` — проверки на корректную сортировку и полноту данных. + +**Особенности реализации:** + +- Массив делится пополам; +- Каждая половина сортируется отдельно, затем сливается в одну; +- Проверка сохранности данных выполняется через XOR всех 64-битных представлений. + +## 5. Проверка корректности + +Корректность результата проверяется двумя способами: + +**Проверка упорядоченности** — проверяем, что числа действительно отсортированы в порядке возрастания: + +```cpp +bool IsSorted(const std::vector &arr) { + for (size_t i = 1; i < arr.size(); ++i) { + if (arr[i - 1] > arr[i]) { + return false; + } + } + return true; +} +``` + +**Проверка сохранения данных** — проверяем, что элементы массива не изменились и не потерялись: + +```cpp +bool SameData(const std::vector &original, const std::vector &result) { + uint64_t hash_original = 0; + uint64_t hash_result = 0; + + for (const double &value : original) { + uint64_t bits = 0; + memcpy(&bits, &value, sizeof(double)); + hash_original ^= bits; + } + + for (const double &value : result) { + uint64_t bits = 0; + memcpy(&bits, &value, sizeof(double)); + hash_result ^= bits; + } + + return hash_original == hash_result; +} +``` + +### Функциональные тесты + +Файл: `tests/functional/main.cpp`, + +| N | Имя теста | Описание | +| ---- | ---------------------- | ------------------------------------ | +| 2 | `2` | Сортировка массива из 2 элементов | +| 3 | `3` | Сортировка массива из 3 элементов | +| 4 | `4` | Сортировка массива из 4 элементов | +| 5 | `5` | Сортировка массива из 5 элементов | +| 10 | `10_1`, `10_2`, `10_3` | Сортировка массива из 10 элементов | +| 17 | `17` | Сортировка массива из 17 элементов | +| 50 | `50` | Сортировка массива из 50 элементов | +| 100 | `100` | Сортировка массива из 100 элементов | +| 1000 | `1000` | Сортировка массива из 1000 элементов | +| 5000 | `5000` | Сортировка массива из 5000 элементов | + +### Пример тестового случая (`N = 10`) + +**Исходный массив:** + +```text +[5.4, -2.1, 0.0, 3.14, -7.8, 1.5, 9.9, -0.4, 8.2, 2.7] +``` + +**Результат после сортировки:** + +```text +[-7.8, -2.1, -0.4, 0.0, 1.5, 2.7, 3.14, 5.4, 8.2, 9.9] +``` + +**Ожидаемый результат проверки:** + +```text +GetOutput() = 1 +``` + +Проверка подтверждает: + +- Корректную сортировку по возрастанию; +- Сохранение всех исходных элементов массива. + +## 6. Экспериментальная среда + +Hardware/OS: + +- процессор: Intel Core i7; +- ядра/потоки: 10 / 16; +- оперативная память: 16 GB; +- операционная система: Windows 11; +- архитектура: x64. + +Toolchain: + +- компилятор: Microsoft Visual C++ (MSVC); +- версия: 19.50.35725.0; +- тип сборки: Release; +- система сборки: CMake. + +**Команды запуска:** + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +**Функциональные тесты:** + +```powershell +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_seq*" +``` + +**Тесты производительности:** + +```powershell +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_seq*" +``` + +## 7. Результаты + +Замер производительности проводился при размере входных данных `k_count = 1000000`. + +| Mode | Count | Time, s | Speedup | Efficiency | +| -------------- | ----- | ---------- | ------- | ---------- | +| seq (pipeline) | 1 | 0.12079842 | 1.00 | 100% | +| seq (task) | 1 | 0.08397980 | 1.00 | 100% | + +## 8. Выводы + +Последовательная реализация (SEQ) выбрана эталоном, потому что: + +- Реализация демонстрирует стабильное время выполнения; +- Алгоритм не зависит от числа потоков, не требует распределения данных и синхронизации. + +Основной ресурсоемкий участок алгоритма — поразрядная сортировка, требующая +многократного прохода по массиву и распределения элементов по корзинам. diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/stl/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/stl/report.md new file mode 100644 index 0000000000..eb3892547e --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/stl/report.md @@ -0,0 +1,230 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием — STL + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Technology: STL +- Variant: 19 + +## 1. Контекст + +В данной задаче последовательная реализация поразрядной сортировки для вещественных +чисел типа `double` переносится на стандартные средства многопоточности C++ (`std::thread`). +Сохраняется вычислительная часть, а именно: преобразование `double` в `uint64_t`, +поразрядная сортировка `RadixSortUInt`, обратное преобразование `uint64_t` в `double`. + +Главное отличие реализации на `std::thread` в том, что потоки создаются и завершаются +вручную в `RunImpl`. Это позволяет явно контролировать разбиение данных. + +## 2. Постановка задачи + +Необходимо выполнить сортировку массива вещественных чисел типа `double` +по возрастанию с использованием стандартных потоков C++ (`std::thread`). + +Постановка задачи STL совпадает с постановкой базовой задачи (SEQ), которая находится +в файле `seq/report.md`. + +### Входные данные + +Целое число `N`, определяющее размер генерируемого массива (`N > 0`): + +```hpp +using InType = int; +``` + +### Выходные данные + +Признак успешной сортировки: + +- `1` — массив отсортирован корректно; +- `0` — обнаружена ошибка сортировки или потеря данных. + +```hpp +using OutType = int; +``` + +## 3. Базовый алгоритм + +Алгоритм (SEQ) состоит из нескольких этапов: + +1. Генерация массива случайных вещественных чисел (`PreProcessingImpl`); +2. Разделение массива на две части; +3. Преобразование чисел `double` в сортируемое представление `uint64_t` (`DoubleToSortable`); +4. Поразрядная сортировка (`RadixSortUInt`); +5. Обратное преобразование в `double` (`SortableToDouble`); +6. Слияние отсортированных частей (`MergeSorted`); +7. Проверка корректности результата. + +## 4. Схема распараллеливания + +### Параллелизуемая область + +Распараллеливание выполняется на этапе локальной сортировки частей массива с помощью `std::thread`. +Потоки создаются вручную через `std::thread`. +Исходный массив делится между потоками на сегменты: + +```cpp +int left_idx = (thread_id * n) / n_threads; +int right_idx = ((thread_id + 1) * n) / n_threads; +``` + +Каждый поток выполняет функцию `PartSort`, которая: + +1. Определяет собственный диапазон элементов; +2. Преобразует значения `double` в `uint64_t`; +3. Выполняет локальную поразрядную сортировку; +4. Выполняет обратное преобразование в `double`; +5. Сохраняет отсортированный результат в `local_results[thread_id]`. + +### Join и синхронизация + +После запуска всех потоков основной поток ожидает их завершения через join(): + +```cpp +for (std::thread& t : threads) { + if (t.joinable()) { + t.join(); + } +} +``` + +Синхронизация через `join()` гарантирует завершение локальной сортировки во всех потоках перед +началом последовательного +слияния результатов. + +## 5. Детали реализации + +**Файлы:** `stl/include/ops_stl.hpp`, `stl/src/ops_stl.cpp` + +| Метод | Назначение | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ValidationImpl` | Проверяет, что количество элементов сортировки строго больше нуля: `GetInput() > 0` | +| `PreProcessingImpl` | Создает `array_` размера `N`, заполняет случайными числами с плавающей точкой в диапазоне `[-100, 100] | +| `RunImpl` | Создает потоки `std::thread`, выполняет локальную сортировку диапазонов и объединяет результаты через MergeSorted | +| `PostProcessingImpl` | Проверяет сортировку (`IsSorted`) и полноту данных (`SameData`), результат этой проверки записывает в `GetOutput()` (если обе проверки успешны = 1, иначе = 0) | + +**Поля класса:** + +- `array_` — исходный массив; +- `result_` — результат после слияния. + +### Карта поток → диапазон + +Каждому потоку соответствует собственный диапазон элементов массива: + +```cpp +int left_idx = (thread_id * n) / n_threads; +int right_idx = ((thread_id + 1) * n) / n_threads; +``` + +### Схема объединения результатов + +После завершения всех потоков локальные отсортированные сегменты +последовательно объединяются функцией MergeSorted: + +```cpp +for (int i = 0; i < n_threads; i++) { + if (!local_results[i].empty()) { + if (result_.empty()) { + result_ = std::move(local_results[i]); + } else { + result_ = MergeSorted(result_, local_results[i]); + } + } +} +``` + +Слияние выполняется последовательно в главном потоке. +На каждом шаге текущий результат объединяется +с очередным локальным отсортированным диапазоном. + +## 6. Проверка корректности + +Используются те же функциональные тесты, что для SEQ, OMP и TBB (`tests/functional/main.cpp`, `N` от 2 до 5000). + +Проверки в `PostProcessingImpl` идентичны SEQ: `IsSorted` + `SameData`, ожидается `GetOutput() == 1`. + +При одинаковом `N` результат должен быть эквивалентен по критерию корректности ( + упорядоченность и сохранность данных). + +**Крайние случаи:** `N <= 0` отсекается в `ValidationImpl`; при `n_threads > n` часть +потоков получает пустой диапазон (`left_idx >= right_idx`) и не пишет в `local_results`. + +## 7. Экспериментальная среда + +Hardware/OS: + +- процессор: Intel Core i7; +- ядра/потоки: 10 / 16; +- оперативная память: 16 GB; +- операционная система: Windows 11; +- архитектура: x64. + +Toolchain: + +- компилятор: Microsoft Visual C++ (MSVC); +- версия: 19.50.35725.0; +- тип сборки: Release; +- система сборки: CMake. + +Число потоков задается переменной окружения `PPC_NUM_THREADS` = {2, 4, 8}. + +**Сборка:** + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +**Функциональные тесты:** + +```powershell +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_stl*" +``` + +**Тесты производительности:** + +```powershell +$env:PPC_NUM_THREADS="T" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_stl*" +``` + +где `T` — число потоков. Для каждого `T` выполнено **3 прогона**; в таблице — среднее время. + +## 8. Результаты + +Замер производительности проводился при размере входных данных `k_count = 1000000`. + +| Mode | Count | Time, s | Speedup | Efficiency | +| ---------- | ----- | ---------- | ------- | ---------- | +| seq (task) | 1 | 0.08397980 | 1.000 | 100.00% | +| seq (pipe) | 1 | 0.12079842 | 1.000 | 100.00% | +| stl (task) | 2 | 0.03192456 | 2.634 | 131.68% | +| stl (pipe) | 2 | 0.04675974 | 2.583 | 129.15% | +| stl (task) | 4 | 0.03874158 | 2.168 | 54.19% | +| stl (pipe) | 4 | 0.05020697 | 2.406 | 60.15% | +| stl (task) | 8 | 0.04166961 | 2.015 | 25.19% | +| stl (pipe) | 8 | 0.05107783 | 2.365 | 29.56% | + +Использование `std::thread` позволило получить ускорение по сравнению с последовательной реализацией. +Наилучший результат достигнут при 2-4 потоках. + +Распределение данных между потоками выполняется равномерно, +так как массив заранее делится на диапазоны близкого размера. +При увеличении числа потоков эффективность снижается из-за: + +- Накладных расходов на создание потоков (`std::thread`); +- Затрат на ожидание завершения потоков через `join()`; +- Последовательного слияния отсортированных частей; +- Роста конкуренции за пропускную способность памяти. + +## 9. Выводы + +Ручная реализация параллелизма с использованием `std::thread` +позволила существенно сократить время выполнения по сравнению +с последовательной версией алгоритма. + +STL-подход дает контроль над созданием потоков, разделением диапазонов и синхронизацией. +Каждый поток независимо обрабатывает собственный сегмент массива, +а объединение результатов выполняется после завершения всех вычислений. + +При дальнейшем увеличении числа потоков накладные расходы +на создание и завершение потоков, а также последовательное слияние результатов начинают ограничивать ускорение. diff --git a/tasks/popova_e_radix_sort_for_double_with_simple_merge/tbb/report.md b/tasks/popova_e_radix_sort_for_double_with_simple_merge/tbb/report.md new file mode 100644 index 0000000000..114ea5850b --- /dev/null +++ b/tasks/popova_e_radix_sort_for_double_with_simple_merge/tbb/report.md @@ -0,0 +1,236 @@ +# Поразрядная сортировка для вещественных чисел (тип double) с простым слиянием — TBB + +- Student: Попова Елизавета Сергеевна, 3823Б1ПР1 +- Technology: TBB +- Variant: 19 + +## 1. Контекст + +В данной задаче последовательная реализация поразрядной сортировки +для вещественных чисел типа `double` переносится на технологию Intel TBB. +Сохраняется вычислительная часть, а именно: преобразование `double` в `uint64_t`, поразрядная +сортировка `RadixSortUInt`, обратное преобразование `uint64_t` в `double`. + +В отличие от последовательной версии, в TBB-реализации массив делится +на количество сегментов, равное числу потоков (`ppc::util::GetNumThreads()`). +Каждый сегмент сортируется независимо в рамках `tbb::parallel_for`. + +После завершения параллельной обработки выполняется последовательное +слияние отсортированных сегментов. + +## 2. Постановка задачи + +Необходимо выполнить сортировку массива вещественных чисел типа `double` +по возрастанию с использованием TBB. + +Постановка задачи TBB совпадает с постановкой базовой задачи (SEQ), которая находится +в файле `seq/report.md`. + +### Входные данные + +Целое число `N`, определяющее размер генерируемого массива (`N > 0`): + +```hpp +using InType = int; +``` + +### Выходные данные + +Признак успешной сортировки: + +- `1` — массив отсортирован корректно; +- `0` — обнаружена ошибка сортировки или потеря данных. + +```hpp +using OutType = int; +``` + +## 3. Базовый алгоритм + +Алгоритм (SEQ) состоит из нескольких этапов: + +1. Генерация массива случайных вещественных чисел (`PreProcessingImpl`); +2. Разделение массива на две части; +3. Преобразование чисел `double` в сортируемое представление `uint64_t` (`DoubleToSortable`); +4. Поразрядная сортировка (`RadixSortUInt`); +5. Обратное преобразование в `double` (`SortableToDouble`); +6. Слияние отсортированных частей (`MergeSorted`); +7. Проверка корректности результата. + +## 4. Схема распараллеливания + +### Параллелизуемая область + +Распараллеливание выполняется на этапе локальной сортировки частей массива с помощью конструкции `tbb::parallel_for`. +Исходный массив делится между потоками на диапазоны. Каждая итерация цикла (от `0` до `n_threads-1`): + +1. Вычисляет собственные индексные границы (`left_idx`, `right_idx`); +2. Преобразует элементы `double` в `uint64_t`; +3. Выполняет локальную поразрядную сортировку; +4. Выполняет обратное преобразование; +5. Сохраняет результат в собственный элемент `local_results`. + +```cpp +int n_threads = std::max(1, ppc::util::GetNumThreads()); + +tbb::parallel_for(0, n_threads, [&](int thread_id) { + int left_idx = (thread_id * n) / n_threads; + int right_idx = ((thread_id + 1) * n) / n_threads; +}); +``` + +### Grainsize и partitioner + +В реализации не используются `blocked_range` и явная настройка `grainsize`. +Работа распределяется между задачами автоматически средствами oneTBB. + +Число одновременно выполняемых потоков ограничивается через +`tbb::global_control(max_allowed_parallelism, n_threads)`. + +В конце параллельной области существует неявный барьер, гарантирующий завершение +локальной сортировки во всех потоках перед началом слияния (аналогично OMP). + +## 5. Детали реализации + +**Файлы:** `tbb/include/ops_tbb.hpp`, `tbb/src/ops_tbb.cpp` + +| Метод | Назначение | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ValidationImpl` | Проверяет, что количество элементов сортировки строго больше нуля: `GetInput() > 0` | +| `PreProcessingImpl` | Создает `array_` размера `N`, заполняет случайными числами с плавающей точкой в диапазоне `[-100, 100]` | +| `RunImpl` | Организует параллельную обработку с помощью `tbb::parallel_for`, выполняет перевод элементов в `uint64_t`, поразрядную сортировку `RadixSort` для `n_threads` сегментов, возвращает данные в формат `double` и выполняет `MergeSorted`| +| `PostProcessingImpl` | Проверяет сортировку (`IsSorted`) и полноту данных (`SameData`), результат этой проверки записывает в `GetOutput()` (если обе проверки успешны = 1, иначе = 0) | + +**Поля класса:** + +- `array_` — исходный массив; +- `result_` — результат после слияния. + +Относительно последовательной версии (SEQ) есть следующие изменения в методе `RunImpl()`: + +1. Последовательная обработка двух половин массива заменена на `tbb::parallel_for`; +2. Для хранения локального результата на каждом потоке создан вектор векторов `local_results`; +3. Каждая задача выполняет локальное копирование своего + участка памяти, преобразование `DoubleToSortable`, вызов `RadixSortUInt` и обратное восстановление типов; +4. После завершения `tbb::parallel_for` выполняется последовательное + слияние промежуточных результатов функцией `MergeSorted`. + +```cpp +int n_threads = std::max(1, ppc::util::GetNumThreads()); + +std::vector> local_results(n_threads); + +tbb::parallel_for(0, n_threads, [&](int thread_id) { + int left_idx = (thread_id * n) / n_threads; + int right_idx = ((thread_id + 1) * n) / n_threads; + + if (left_idx < right_idx) { + local_results[thread_id] = ...; + } +}); + +for (int i = 0; i < n_threads; ++i) + if (!local_results[i].empty()) + result_ = MergeSorted(result_, local_results[i]); +``` + +### Локальные результаты и работа с памятью + +Для предотвращения гонок данных каждый поток записывает результат +только в собственный элемент `local_results[thread_id]`. + +Во время локальной сортировки используются временные буферы +`std::vector local_bits`, содержащие преобразованные +значения `double`. + +После завершения `tbb::parallel_for` выполняется последовательное +объединение локальных результатов в итоговый массив `result_` +с помощью функции `MergeSorted`. + +## 6. Проверка корректности + +Используются те же функциональные тесты, что для SEQ и OMP +(`tests/functional/main.cpp`, `N` от 2 до 5000). + +Проверки в `PostProcessingImpl` идентичны SEQ: `IsSorted` + `SameData`, ожидается `GetOutput() == 1`. + +При одинаковом `N` результат должен быть эквивалентен по критерию корректности +(упорядоченность и сохранность данных). + +## 7. Экспериментальная среда + +Hardware/OS: + +- процессор: Intel Core i7; +- ядра/потоки: 10 / 16; +- оперативная память: 16 GB; +- операционная система: Windows 11; +- архитектура: x64. + +Toolchain: + +- компилятор: Microsoft Visual C++ (MSVC); +- версия: 19.50.35725.0; +- тип сборки: Release; +- система сборки: CMake. + +Число потоков задается переменной окружения `PPC_NUM_THREADS` = {1, 2, 4, 8}. + +**Команды запуска:** + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release --parallel +``` + +**Функциональные тесты:** + +```powershell +./build/bin/ppc_func_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_tbb*" +``` + +**Тесты производительности:** + +```powershell +$env:PPC_NUM_THREADS="T" +./build/bin/ppc_perf_tests.exe --gtest_filter="*popova_e_radix_sort_for_double_with_simple_merge_threads_tbb*" +``` + +где `T` — количество потоков. + +## 8. Результаты + +Замер производительности проводился при размере входных данных `k_count = 1000000`. + +| Mode | Count | Time, s | Speedup | Efficiency | +| ---------- | ----- | ---------- | ------- | ---------- | +| seq (task) | 1 | 0.08397980 | 1.000 | 100.00% | +| seq (pipe) | 1 | 0.12079842 | 1.000 | 100.00% | +| tbb (task) | 2 | 0.03794540 | 2.213 | 110.65% | +| tbb (pipe) | 2 | 0.04815750 | 2.508 | 125.42% | +| tbb (task) | 4 | 0.03377934 | 2.486 | 62.15% | +| tbb (pipe) | 4 | 0.04839584 | 2.496 | 62.40% | +| tbb (task) | 8 | 0.03927820 | 2.138 | 26.73% | +| tbb (pipe) | 8 | 0.05713506 | 2.114 | 26.43% | + +Использование oneTBB позволило получить ускорение по сравнению с последовательной реализацией. +Наилучший результат достигнут при 2-4 потоках. + +Распределение работы между задачами остается достаточно равномерным, +поскольку массив заранее делится на диапазоны близкого размера. +При увеличении числа потоков эффективность снижается из-за роста overhead +на управление задачами и последовательного этапа слияния результатов. + +## 9. Выводы + +Технология Intel TBB позволила существенно сократить время выполнения по сравнению +с последовательной реализацией (более чем в 2 раза) и упростить организацию +параллельной обработки с помощью `tbb::parallel_for`. TBB удобно использовать для задач +с независимыми участками вычислений, где требуется автоматическое распределение нагрузки между потоками +и минимизация ручного управления параллелизмом. + +Наибольший выигрыш наблюдается при умеренном числе потоков (2–4). +При дальнейшем увеличении числа потоков overhead на управление задачами, +последовательное слияние результатов и ограничения пропускной способности +памяти начинают ограничивать масштабируемость алгоритма. В таком случае накладные расходы +перевешивают выигрыш от параллельной обработки.