Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <vector>

#include "popova_e_radix_sort_for_double_with_simple_merge/common/include/common.hpp"
#include "task/include/task.hpp"

namespace popova_e_radix_sort_for_double_with_simple_merge_threads {

class PopovaERadixSorForDoubleWithSimpleMergeALL : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kALL;
}
explicit PopovaERadixSorForDoubleWithSimpleMergeALL(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;

std::vector<double> array_; // массив для сортировки
std::vector<double> result_; // результат сортировки
};

} // namespace popova_e_radix_sort_for_double_with_simple_merge_threads
265 changes: 265 additions & 0 deletions tasks/popova_e_radix_sort_for_double_with_simple_merge/all/report.md
Original file line number Diff line number Diff line change
@@ -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`;
- Конкуренцию за память и кэш.

Для небольших задач использование большого числа процессов и потоков
может быть неэффективным, поскольку накладные расходы
перевешивают выигрыш от параллельной обработки.
Loading
Loading