Skip to content
354 changes: 354 additions & 0 deletions tasks/krasnopevtseva_v_hoare_batcher_sort/all/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
# Гибридная сортировка Хоара-Бэтчера (Quick Batcher Sort)

- Студент: Краснопевцева Вероника Дмитриевна, группа 3823Б1ПМоп3
- Технология: ALL (STL + OMP)
- Вариант: 14

## 1. Введение

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

Целью данной работы является реализация комбинированного параллельного
гибридного алгоритма, использующего возможности как OpenMP, так и STL,
сочетающего адаптивный итеративный QuickSort с последующим слиянием
Бэтчера для улучшения общей производительности. Ожидаемый результат —
устойчивая работа алгоритма на массивах различных размеров с
использованием оптимизаций, таких как сортировка вставками для малых
подмассивов и эффективное параллельное слияние, а также адаптивный выбор
стратегии в зависимости от размера данных.

## 2. Постановка задачи

### Формальное определение

Необходимо реализовать комбинированный параллельный алгоритм сортировки
массива целых чисел на основе комбинации алгоритмов Хоара и Бэтчера с
использованием технологий OpenMP и STL.

### Входные данные

- Вектор целых чисел `std::vector<int>` произвольной длины.

### Выходные данные

- Отсортированный по неубыванию вектор целых чисел.

### Ограничения

- Алгоритм должен корректно обрабатывать массивы любого размера.
- Для малых подмассивов используется сортировка вставками.
- Параллельная обработка выполняется на этапе начальной сортировки
подмассивов и на этапе слияния.
- Количество потоков определяется через `omp_get_max_threads()`.
- Реализована адаптивная стратегия: для малых массивов (n < 1000)
используется последовательная сортировка.

## 3. Базовый алгоритм (последовательная версия)

### 3.1 Быстрая сортировка (итеративная версия)

Используется нерекурсивная реализация через стек:

1. В стек помещается пара `(left, right)` для всего массива.
2. Пока стек не пуст:
- Извлекается диапазон `[l, r]`.
- Если `l >= r` — пропуск.
- Если размер диапазона небольшой - вызывается сортировка вставками.
- Выбирается опорный элемент (последний элемент массива).
- Выполняется разбиение Хоара.
- Меньший поддиапазон помещается в стек первым (для балансировки).

### 3.2 Сортировка вставками

Для малых подмассивов (размер < 16) применяется классическая сортировка
вставками, так как она показывает хорошие результаты на почти
отсортированных данных и малых размерах.

### 3.3 Слияние Бэтчера

После завершения быстрой сортировки выполняется слияние отсортированных
блоков с использованием алгоритма Бэтчера с последовательным применением
`std::inplace_merge`.

## 4. Схема параллелизации

### 4.1 Общая стратегия

Параллелизация реализована по принципу "разделяй и властвуй" с
комбинированным использованием OpenMP и STL:

1. **Адаптивный выбор стратегии**: Если размер массива менее 1000
элементов, используется последовательная сортировка (QuickSort),
иначе — параллельная.
2. **Определение количества потоков**: Количество потоков определяется
через `omp_get_max_threads()`.
3. **Разбиение данных**: Исходный массив разделяется на `N` блоков,
где `N` — количество доступных потоков.
4. **Параллельная сортировка**: Каждый блок сортируется с
использованием директивы `#pragma omp parallel for`.
5. **Параллельное слияние**: Отсортированные блоки попарно сливаются
с использованием иерархической схемы Бэтчера, где каждая пара
сливается параллельно через OpenMP.

### 4.2 Распределение данных

- Размер блока для каждого потока: `base_size = n / numthreads`
- Остаток от деления добавляется к последнему блоку:
`last_size = base_size + remainder`
- Каждый поток получает указатель на начало своего блока и его размер
- OpenMP автоматически распределяет итерации по доступным потокам

### 4.3 Коммуникационная схема

- На этапе сортировки: блоки сортируются независимо, обмен данными
отсутствует.
- На этапе слияния: выполняется попарное слияние блоков с
использованием `std::inplace_merge`.
- Слияние организовано в виде иерархического дерева (парное слияние
на каждом уровне).
- OpenMP обеспечивает параллельное выполнение операций слияния на
каждом уровне.

### 4.4 Синхронизация

- Используется `#pragma omp parallel for` с явным указанием
shared-переменных.
- Отсутствие гонок данных гарантируется тем, что каждый поток работает
со своим выделенным диапазоном памяти.
- На этапе слияния используется условное распараллеливание через
параметр `par_if_greater` (порог 32).
- Адаптивная стратегия позволяет избежать накладных расходов на малых
массивах.

## 5. Детали реализации

### 5.1 Структура кода

- **Файл**: `krasnopevtseva_v_hoare_batcher_sort/all/include/ops_all.hpp`,
`krasnopevtseva_v_hoare_batcher_sort/all/ops_all.cpp`
- **Пространство имён**: `krasnopevtseva_v_hoare_batcher_sort`
- **Класс**: `KrasnopevtsevaVHoareBatcherSortALL`

### 5.2 Основные методы

|Метод|Назначение|
|-|-|
|`ValidationImpl()`|Проверка входных данных (массив не пуст)|
|`PreProcessingImpl()`|Инициализация выходного вектора|
|`RunImpl()`|Запуск комбинированной сортировки|
|`SortLocalData()`|Адаптивный выбор стратегии сортировки|
|`ParallelSortChunks()`|Параллельная сортировка блоков|
|`QuickSort()`|Итеративная быстрая сортировка|
|`Partition()`|Разбиение Хоара|
|`InsertionSort()`|Сортировка вставками|
|`BatcherMergeBlocksStep()`|Слияние двух блоков|
|`BatcherMerge()`|Параллельное слияние блоков|

### 5.3 Важные особенности

- **Адаптивная стратегия**: Для массивов размером менее 1000 элементов
используется последовательная сортировка, что позволяет избежать
накладных расходов на создание потоков.
- **Выбор опорного элемента**: Используется последний элемент массива,
что упрощает реализацию.
- **Порог сортировки вставками**: 16 элементов.
- **Условное распараллеливание слияния**: Слияние выполняется параллельно
только если `(thread_input_size / step) > 32`.
- **Итеративный QuickSort**: Избегает переполнения стека вызовов.
- **Комбинированный подход**: Сочетает директивы OpenMP для параллельных
регионов и стандартную библиотеку для базовых операций.

### 5.4 Использование памяти

- Исходный массив сортируется на месте.
- Для отслеживания границ блоков используются массивы `pointers` и
`sizes`.
- OpenMP управляет созданием и распределением потоков.
- Дополнительная память: O(numthreads) для хранения указателей на
блоки.

## 6. Окружение и тестирование

### 6.1 Аппаратное и программное обеспечение

- **Процессор**: AMD Ryzen 7 5700X 8-Core Processor
- **ОЗУ**: 32.0 ГБ
- **ОС**: Windows 11 Pro 25H2
- **Набор инструментов**: DevContainer с компилятором GCC
(Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
- **Тип сборки**: Release
- **Фреймворк тестирования**: Google Test (gtest)
- **Технологии**: OpenMP + STL

### 6.2 Переменные окружения

- `OMP_NUM_THREADS` — количество потоков OpenMP

### 6.3 Тестовые данные и верификация

#### Функциональное тестирование

Для проверки базовой корректности алгоритма был разработан набор из 6
тестовых сценариев, покрывающих различные типы входных данных:

|№|Тип массива|Описание|Размер|
|-|-|-|-:|
|1|`sorted_array`|Уже отсортированный по возрастанию массив|16|
|2|`default_array`|Случайный массив с произвольным порядком|20|
|3|`short_array`|Короткий массив минимального размера|4|
|4|`long_array`|Длинный массив с повторениями|50|
|5|`two_number_array`|Массив только из двух значений (10 и 20)|9|
|6|`end_to_begin_array`|Массив, отсортированный по убыванию|24|

#### Верификация результата

Для каждого тестового набора проверялось выполнение основного инварианта
сортировки:

- Для всех индексов `i` от 0 до `size-2` выполняется условие
`output[i] <= output[i+1]`

Дополнительно проводилось сравнение с эталонной реализацией `std::sort`
на тех же входных данных для всех тестовых случаев.

#### Тестирование производительности

Для оценки производительности был реализован отдельный тестовый набор,
генерирующий массив из **100 000** случайных целых чисел в диапазоне
`[0, 100 000 000]`. Генерация выполняется с использованием генератора
псевдослучайных чисел `std::mt19937` для обеспечения воспроизводимости
результатов.

**Результаты корректности:** Все 6 функциональных тестов успешно пройдены
для ALL-версии. Перфоманс-тесты также подтверждают корректную сортировку
массива из 100 000 элементов.

## 7. Результаты и обсуждение

### 7.1 Корректность

Корректность комбинированной реализации на ALL (OpenMP + STL) подтверждена:

- Сравнением результатов с последовательной версией на идентичных
входных данных.
- Проверкой инварианта отсортированности для всех выходных массивов.
- Успешным прохождением всех функциональных тестов.
- Проверкой адаптивной стратегии на массивах различных размеров.

### 7.2 Производительность

Результаты для массива из **100 000** случайных целых чисел:

|Реализация|Потоков|Время, мс|Ускорение|Эффективность|
|-|-:|-:|-:|-:|
|SEQ|1|32.0|1.00|N/A|
|ALL|8|22.0|1.45|18.2%|

### 7.3 Анализ производительности

#### Наблюдаемые эффекты

- **Ускорение ALL версии около 1.45x** при использовании 8 потоков.
Это соответствует эффективности около 18.2%.

#### Факторы, влияющие на производительность ALL версии

|Фактор|Влияние на ALL версию|
|-|-|
|Адаптивная стратегия (n < 1000)|Положительное|
|Накладные расходы OpenMP|Умеренные|
|Параллельная сортировка блоков|Хорошее ускорение|
|Параллельное слияние|Умеренное ускорение|
|Балансировка нагрузки|Средняя|

## 8. Заключения

### Основные выводы

1. Разработан комбинированный параллельный гибридный алгоритм сортировки
с использованием OpenMP и STL, сочетающий QuickSort, InsertionSort
и BatcherMerge.
2. Алгоритм корректен и демонстрирует ускорение до 1.45x на 8 потоках
для массива из 100 000 элементов.
3. Реализована адаптивная стратегия: для массивов размером менее 1000
элементов используется последовательная сортировка, что позволяет
избежать накладных расходов на создание потоков.
4. Сортировка вставками для подмассивов размера < 16 даёт выигрыш по
сравнению с рекурсивным QuickSort.
5. Условное распараллеливание слияния позволяет адаптироваться под
размер обрабатываемых данных.

## Приложение

```cpp
bool KrasnopevtsevaVHoareBatcherSortALL::RunImpl() {
const auto &input = GetInput();
std::vector<int> result = input;

if (result.size() <= 1) {
GetOutput() = result;
return true;
}

SortLocalData(result);
GetOutput() = std::move(result);
return true;
}

void KrasnopevtsevaVHoareBatcherSortALL::SortLocalData(std::vector<int> &data) {
int n = static_cast<int>(data.size());
if (n <= 0) {
return;
}

int numthreads = omp_get_max_threads();
numthreads = std::min(n, numthreads);

if (n < 1000) {
QuickSort(data, 0, n - 1);
} else {
ParallelSortChunks(data, n, numthreads);
}
}

void KrasnopevtsevaVHoareBatcherSortALL::ParallelSortChunks(
std::vector<int> &arr, int n, int numthreads) {
if (n <= 0) {
return;
}

numthreads = std::min(n, numthreads);
if (numthreads <= 0) {
numthreads = 1;
}

int thread_input_size = n / numthreads;
int thread_input_remainder_size = n % numthreads;

std::vector<int *> pointers(numthreads);
std::vector<int> sizes(numthreads);

for (int i = 0; i < numthreads; ++i) {
std::ptrdiff_t offset = static_cast<std::ptrdiff_t>(i) *
static_cast<std::ptrdiff_t>(thread_input_size);
pointers[i] = arr.data() + offset;
sizes[i] = thread_input_size;
}
sizes.back() += thread_input_remainder_size;

#pragma omp parallel for default(none) shared(arr, pointers, sizes, numthreads)
for (int i = 0; i < numthreads; ++i) {
int left = static_cast<int>(pointers[i] - arr.data());
int right = left + sizes[i] - 1;
if (left < right) {
QuickSort(arr, left, right);
}
}

BatcherMerge(thread_input_size, pointers, sizes, 32);
}
Loading
Loading