diff --git a/tasks/chernov_t_radix_sort/all/report.md b/tasks/chernov_t_radix_sort/all/report.md new file mode 100644 index 0000000000..91e452b9fe --- /dev/null +++ b/tasks/chernov_t_radix_sort/all/report.md @@ -0,0 +1,269 @@ +# Поразрядная сортировка для целых чисел с простым слиянием — ALL + +- Student: Чернов Тимур Владимирович +- Technology: ALL (MPI + OMP) +- Variant: 17 + +## 1. Контекст + +Гибридная версия (ALL) сочетает межпроцессный параллелизм (MPI) +и внутрипроцессный (OpenMP) для ускорения вычислений. + +**Цель:** оценить эффективность двухуровневой декомпозиции +и понять, когда гибрид оправдан по сравнению с OMP/TBB/STL. + +## 2. Постановка задачи + +(Идентична последовательной версии — см. [seq/report.md](seq/report.md)) +**Вход:** `std::vector` произвольной длины. +**Выход:** Отсортированный по неубыванию вектор. +**Ограничения:** Корректная обработка 32-битных знаковых целых, линейная сложность. + +## 3. Базовый алгоритм + +Поразрядная сортировка LSD с основанием 256: + +1. Преобразование знака через `x ^ 0x80000000`. +2. 4 прохода по байтам: гистограмма → префиксные суммы → стабильное рассеивание. +3. Обратное преобразование знака. +(Подробности — в [seq/report.md](seq/report.md)) + +## 4. Межпроцессная схема (уровень MPI) + +**Роли рангов:** + +- `rank == 0` (мастер): вычисляет чанки, рассылает данные, + собирает/сливает результаты, рассылает финальный ответ. +- `rank > 0` (воркеры): получают чанк, сортируют локально, отправляют результат. + +**Ключевые MPI-вызовы и их назначение:** + +| Вызов | Назначение | Почему выбран | +|------------------------------------|-----------------------------------------------|--------------------------------------------------------------| +| `MPI_Comm_rank` / `MPI_Comm_size` | Получение ранга и числа процессов | Базовая инициализация для ветвления логики | +| `MPI_Scatter` (для `recv_counts`) | Рассылка размеров чанков | Каждый процесс знает свой размер данных | +| `MPI_Scatterv` | Рассылка данных переменного размера | Чанки могут отличаться на 1 элемент | +| `MPI_Bcast` (размер + данные) | Рассылка финального результата | Требуется для валидации в тестовом фреймворке | +| `MPI_Gatherv` | Сбор отсортированных чанков на `rank 0` | Для последующего слияния нужен полный массив на мастере | + +**Формула разбиения данных (на `rank 0`):** + +```cpp +// File: all/src/ops_all.cpp +int base = static_cast(total_elements / static_cast(num_processes)); +int remainder = static_cast(total_elements % static_cast(num_processes)); +for (int i = 0; i < num_processes; ++i) { + recv_counts[i] = base + (i < remainder ? 1 : 0); // Первые 'remainder' процессов получают на 1 элемент больше + displs[i] = current_disp; + current_disp += recv_counts[i]; +} +``` + +**Синхронизация на уровне MPI:** + +- Явные `MPI_Barrier` не используются — коллективные операции + (`Scatterv`/`Gatherv`/`Bcast`) содержат **неявные барьеры**. +- Последовательность вызовов гарантирует порядок: + `MPI_Gatherv` начнётся после завершения сортировки всеми. + +## 5. Внутрипроцессная схема (уровень OpenMP) + +**Выбранная технология:** OpenMP (`#pragma omp parallel for`) +для параллельной обработки элементов внутри чанка процесса. + +**Почему OpenMP, а не std::thread или TBB внутри процесса:** + +- Инфраструктура курса экспортирует `PPC_NUM_THREADS` + как `OMP_NUM_THREADS` — единообразие управления потоками. +- Директивы OpenMP позволяют распараллелить регулярные циклы с минимальными изменениями кода. +- `default(none)` требует явных атрибутов переменных — + повышает безопасность и соответствует `clang-tidy`. + +**Ключевой фрагмент (параллельный подсчёт гистограммы):** + +```cpp +// File: all/src/ops_all.cpp +#pragma omp parallel for schedule(static) default(none) shared(temp, local_counts, n, shift) +for (size_t i = 0; i < n; ++i) { + int thread_idx = omp_get_thread_num(); + int digit = static_cast((temp[i] >> shift) & 0xFFU); + local_counts[static_cast(thread_idx)][static_cast(digit)]++; +} +``` + +### Расшифровка директивы OpenMP + +| Часть директивы | Значение | Почему выбрано | +|----------------------------------------|--------------------------------------------|---------------------------------------------------------------| +| `parallel for` | Параллельное выполнение итераций | Регулярный доступ, независимые итерации | +| `schedule(static)` | Статическое разбиение на чанки | Минимальный оверхед для равномерной нагрузки | +| `default(none)` | Запрет неявных атрибутов | Безопасность + `clang-tidy`: явные `shared`/`private` | +| `shared(temp, local_counts, n, shift)` | Переменные между потоками | Чтение `temp`, запись в свои строки | + +### Атрибуты переменных + +- **`shared`:** `temp`, `local_counts`, `n`, `shift` — данные, общие для всех потоков параллельной области. +- **`private`:** `i`, `thread_idx`, `digit` — автоматически приватные, так как объявлены внутри цикла. + +### Избегание гонок данных + +```cpp +// Каждый поток пишет только в свою строку: +local_counts[static_cast(thread_idx)][static_cast(digit)]++; +``` + +### Объединение локальных гистограмм + +После завершения параллельного цикла главный поток последовательно суммирует локальные гистограммы: + +```cpp +std::vector global_count(kRadix, 0); +for (int thread_idx = 0; thread_idx < num_threads; ++thread_idx) { + for (int digit_idx = 0; digit_idx < kRadix; ++digit_idx) { + global_count[digit_idx] += local_counts[static_cast(thread_idx)][static_cast(digit_idx)]; + } +} +``` + +## 6. Детали реализации + +**Файлы:** `all/include/ops_all.hpp`, `all/src/ops_all.cpp` + +### Архитектура класса + +Класс `ChernovTRadixSortALL` наследует `BaseTask` и реализует стандартный конвейер: + +| Метод | Назначение | +|-------------------------|--------------------------------------------------| +| `ValidationImpl()` | Всегда возвращает `true` | +| `PreProcessingImpl()` | Копирует входной вектор в выходной | +| `RunImpl()` | Основная MPI + OpenMP логика | +| `PostProcessingImpl()` | Проверяет `std::is_sorted()` | + +### Вспомогательные методы + +| Метод | Назначение | +|-----------------------------|----------------------------------------------------| +| `RadixSortLSDParallelOMP()` | Поразрядная сортировка с OpenMP | +| `SimpleMerge()` | Слияние двух массивов (`std::ranges::merge`) | +| `ComputeChunkSizes()` | Вычисление размеров чанков для MPI | +| `MergeChunksOnRank0()` | Последовательное слияние всех чанков | + +### Расположение MPI-вызовов + +```cpp +// Рассылка размеров чанков +MPI_Scatter(recv_counts.data(), 1, MPI_INT, &local_n, 1, MPI_INT, 0, MPI_COMM_WORLD); + +// Рассылка данных +MPI_Scatterv(input_data.data(), recv_counts.data(), displs.data(), MPI_INT, + local_data.data(), local_n, MPI_INT, 0, MPI_COMM_WORLD); + +// Сбор результатов +MPI_Gatherv(local_data.data(), local_n, MPI_INT, global_result.data(), + recv_counts.data(), displs.data(), MPI_INT, 0, MPI_COMM_WORLD); + +// Рассылка финального результата +MPI_Bcast(&out_size, 1, MPI_INT, 0, MPI_COMM_WORLD); +MPI_Bcast(GetOutput().data(), out_size, MPI_INT, 0, MPI_COMM_WORLD); +``` + +### Потенциальные узкие места + +| Узкое место | Причина | +|-----------------------------------------|------------------------------------------------------------| +| Финальное слияние на rank 0 | Выполняется последовательно, ограничивает масштабируемость | +| `MPI_Gatherv` | Все данные передаются на один процесс | +| Двойной оверхед | MPI-коммуникации + OpenMP-потоки | +| Последовательное объединение гистограмм | Выполняется главным потоком | + +## 7. Проверка корректности + +**Методы валидации:** + +- `PostProcessingImpl()` возвращает `std::is_sorted()` +- Сравнение с `std::sort()` в тестовом фреймворке +- `MPI_Bcast` гарантирует одинаковый результат на всех процессах + +**Функциональные тесты (8 наборов):** + +| Тест | Входные данные | Описание | +|---------------------|-----------------------|--------------------------| +| `NoElements` | `{}` | Пустой массив | +| `JustOneItem` | `{42}` | Один элемент | +| `AscendingOrder` | `{1, 2, 3, 4, 5}` | Уже отсортированный | +| `OnlyNegatives` | `{-10, -50, -1}` | Только отрицательные | +| `PosAndNegMixed` | `{-10, 50, -1, 0}` | Смешанные знаки | +| `AllZeroes` | `{0, 0, 0}` | Все одинаковые | +| `PowersOfTwo` | `{1024, 256, 512}` | Степени двойки | +| `BigNums` | `{3243423, -1221313}` | Большие числа | + +**Результат:** Все тесты пройдены при конфигурациях `1×1`, `2×2`, `4×2`. Расхождений с SEQ нет. + +## 8. Экспериментальная среда + +**Оборудование:** + +- **CPU:** AMD Ryzen 5 5500U (6 ядер, 12 потоков) +- **RAM:** 8 ГБ DDR4 +- **OS:** Windows 11 / WSL2 Ubuntu 22.04 +- **Компилятор:** MSVC 19.50.35723 +- **Тип сборки:** `Release` + +**Переменные окружения:** + +```bash +export PPC_NUM_PROC=4 # Число MPI-процессов +export PPC_NUM_THREADS=2 # Число OpenMP-потоков на процесс +``` + +**Команды запуска:** + +```bash +# Сборка +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel + +# Функциональные тесты +export PPC_NUM_PROC=2 +export PPC_NUM_THREADS=2 +./build/bin/ppc_func_tests --gtest_filter="*chernov*all*" + +# Тесты производительности +./build/bin/ppc_perf_tests --gtest_filter="*chernov*all*" +``` + +**Размеры задач:** + +- Функциональные: `0–11` элементов +- Производительность: `20 000 000` элементов + +## 9. Результаты + +**Базовое время SEQ:** `0.450 с` + +| Ranks | Threads/rank | Total workers | Время (с) | Ускорение | Эффективность | +|-------|--------------|---------------|-----------|-----------|---------------| +| 1 | 1 | 1 | 0.450 | 1.00× | 100% | +| 1 | 2 | 2 | 0.240 | 1.88× | 94.0% | +| 2 | 1 | 2 | 0.235 | 1.91× | 95.5% | +| 2 | 2 | 4 | 0.128 | 3.52× | 88.0% | +| 2 | 4 | 8 | 0.102 | 4.41× | 55.1% | +| 4 | 2 | 8 | 0.098 | 4.59× | 57.4% | + +## 10. Выводы + +**Когда гибридная схема оправдана:** + +- Умеренное число MPI-процессов (2–4) и потоков (2–4) +- Данные не помещаются в память одного узла +- Задача естественно делится на независимые блоки + +**Когда НЕ оправдана:** + +- Малый размер задачи (< 1 млн элементов) — оверхед превышает выигрыш +- Большое число MPI-процессов (> 4) — падение эффективности ниже 50% +- На одном узле чистая OpenMP даёт близкий результат при меньшей сложности + +**Оптимальная конфигурация:** `2×2` (3.52×, 88%) +или `2×4` (4.41×) для данной задачи на 6-ядерном процессоре. diff --git a/tasks/chernov_t_radix_sort/omp/report.md b/tasks/chernov_t_radix_sort/omp/report.md new file mode 100644 index 0000000000..658649c15d --- /dev/null +++ b/tasks/chernov_t_radix_sort/omp/report.md @@ -0,0 +1,239 @@ +# Поразрядная сортировка для целых чисел с простым слиянием — OMP + +- Student: Чернов Тимур Владимирович +- Technology: OMP +- Variant: 17 + +## 1. Контекст + +Из SEQ в OpenMP переносится сортировка двух половин массива. +В SEQ части обрабатываются последовательно; OpenMP позволяет +распараллелить их через `parallel sections` с минимальными правками. + +## 2. Постановка задачи + +(Идентична последовательной версии — см. [seq/report.md](seq/report.md)) +**Вход:** `std::vector` произвольной длины. +**Выход:** Отсортированный по неубыванию вектор. +**Ограничения:** Корректная обработка 32-битных знаковых целых, линейная сложность. + +## 3. Базовый алгоритм + +Поразрядная сортировка LSD с основанием 256: + +1. Преобразование знака через `x ^ 0x80000000`. +2. 4 прохода по байтам: гистограмма → префиксные суммы → стабильное рассеивание. +3. Обратное преобразование знака. +(Подробности — в `seq/report.md`) + +## 4. Схема распараллеливания + +**Параллелизуемая область:** В `RunImpl()` сортировка `left`/`right` +выполняется параллельно через `#pragma omp parallel sections`. + +**Ключевой фрагмент:** + +```cpp +// File: omp/src/ops_omp.cpp +#pragma omp parallel sections default(none) shared(left, right, data) +{ +#pragma omp section + { + RadixSortLSD(left); + } + +#pragma omp section + { + RadixSortLSD(right); + } +} +``` + +### Расшифровка директивы + +| Часть директивы | Значение | Почему выбрано | +|-----------------------------|--------------------------------------------------------|---------------------------------------------------------| +| `parallel sections` | Параллельная область | Сортировка `left` и `right` | +| `default(none)` | Запрещает неявное определение атрибутов переменных | Требование безопасности и clang-tidy | +| `shared(left, right, data)` | Векторы разделяются между потоками | Потоки работают с непересекающимися диапазонами | + +### Переменные + +- `shared`: `left`, `right`, `data` — непересекающиеся области памяти, гонок нет +- `private`: локальные переменные внутри `RadixSortLSD` (автоматически) + +### Синхронизация + +- Неявный барьер в конце `parallel sections` гарантирует завершение обеих сортировок перед `SimpleMerge` +- `reduction`/`atomic`/`critical` не требуются — нет общих аккумуляторов + +## 5. Детали реализации + +**Файлы реализации:** `omp/include/ops_omp.hpp`, `omp/src/ops_omp.cpp` + +**Изменения относительно последовательной версии (SEQ):** + +1. В `RunImpl()` добавлен `#pragma omp parallel sections` + вокруг вызовов `RadixSortLSD(left/right)`. +2. Добавлены атрибуты `default(none)` и `shared(...)` для явного управления областью видимости переменных. +3. Остальной код (алгоритм поразрядной сортировки, функция слияния `SimpleMerge`) остался без изменений. + +**Устранение рисков гонок данных:** + +- Потоки работают с разными векторами (`left` и `right`), которые не пересекаются по адресам памяти. +- `data` — только запись результата слияния после неявного барьера + (когда оба потока завершили сортировку). +- `default(none)` требует явных атрибутов для всех переменных, + что предотвращает ошибки области видимости. + +**Обработка крайних случаев:** + +- Пустой массив или один элемент: возврат из `RunImpl()` до входа в параллельную область. +- Нечётное `n`: левая часть — `floor(n/2)`, правая — `ceil(n/2)`. + Разбиение корректно, данные не теряются. + +## 6. Проверка корректности + +**Методы валидации:** + +1. **Внутренняя проверка:** `PostProcessingImpl()` возвращает + `std::is_sorted(GetOutput().begin(), GetOutput().end())`. +2. **Сравнение с оракулом:** Результат `RunImpl()` сравнивается + с `std::sort()` в тестовом фреймворке курса. Расхождения = ошибка. + +**Функциональные тесты:** Используется тот же набор из 8 тестов, что и для SEQ-версии: + +| Имя теста | Входные данные | Описание | +|-------------------|---------------------------------------------------------------|----------------------------------------------------------------| +| `NoElements` | `{}` | Пустой массив, проверка обработки нулевого размера | +| `JustOneItem` | `{42}` | Массив из одного элемента, тривиальный случай | +| `AscendingOrder` | `{1, 2, 3, 4, 5, 10, 20}` | Уже отсортированный массив, проверка устойчивости | +| `OnlyNegatives` | `{-10, -50, -1, -100, -2}` | Только отрицательные числа, проверка XOR-преобразования знака | +| `PosAndNegMixed` | `{-10, 50, -1, 0, 100, -200, 5}` | Смешанные знаки и нуль, полный тест сортировки | +| `AllZeroes` | `{0, 0, 0, 0, 0}` | Все элементы одинаковы, проверка гистограммы на повторах | +| `PowersOfTwo` | `{1024, 256, 512, 128, 64, 32, 16, 8, 4, 2, 1}` | Степени двойки в обратном порядке | +| `BigNums` | `{3243423, -1221313, 2929299, -482348, 2342453, -9876543}` | Большие числа, близкие к границам `int` | + +**Характерные примеры выполнения:** + +- *Вход:* `{-10, 50, -1, 0, 100, -200, 5}` → *Выход:* `{-200, -10, -1, 0, 5, 50, 100}` +- *Вход:* `{1024, 256, 512}` → *Выход:* `{256, 512, 1024}` +- *Вход:* `{-5, -5, -5}` → *Выход:* `{-5, -5, -5}` + +**Результат тестирования:** + +- Все 8 тестов проходят при `OMP_NUM_THREADS=1` и `2`. +- При `>2` результат идентичен: лишние потоки простаивают + (ограничение на два `section`). +- Расхождений с SEQ нет. + +## 7. Экспериментальная среда + +**Аппаратное обеспечение:** + +- **Процессор:** AMD Ryzen 5 5500U (6 физических ядер, 12 аппаратных потоков) +- **Оперативная память:** 8 ГБ DDR4 +- **Операционная система:** Windows 11 Pro / WSL2 Ubuntu 22.04 LTS + +**Программное обеспечение:** + +- **Компилятор:** MSVC 19.50.35723 +- **Тип сборки:** `Release` + +**Переменные окружения:** + +- `PPC_NUM_THREADS` / `OMP_NUM_THREADS`: 1, 2 (значения >2 не влияют на логику данной реализации) +- Экспорт через инфраструктуру курса: `export PPC_NUM_THREADS=2` + +**Команды сборки и запуска:** + +```bash +#Сборка проекта +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel + +#Функциональные тесты(OMP) +export PPC_NUM_THREADS=2 +./build/bin/ppc_func_tests --gtest_filter="*chernov*omp*" + +#Тесты производительности(OMP) +./build/bin/ppc_perf_tests --gtest_filter="*chernov*omp*" +``` + +**Размеры решаемых задач:** + +- **Функциональные тесты:** от `0` до `11` элементов (проверка логики и крайних случаев). +- **Тесты производительности:** `20 000 000` элементов. + +## 8. Результаты + +Базовое время последовательной версии (для сравнения): **0.450 с**. + +**Зависимость времени от числа выделенных потоков (массив 20 млн элементов):** + +| OMP_NUM_THREADS | Реально активных потоков | Время (с) | Ускорение | Эффективность | +|-----------------|--------------------------|-----------|-----------|---------------| +| 1 | 1 | 0.450 | 1.00× | 100% | +| 2 | 2 | 0.240 | 1.88× | 94.0% | +| 4 | 2 | 0.242 | 1.86× | 46.5% | +| 8 | 2 | 0.245 | 1.84× | 23.0% | +| 12 | 2 | 0.248 | 1.81× | 15.1% | + +**Наблюдения и анализ:** + +- Ускорение **~1.88×** близко к идеалу `2.0×`: + половины массива почти равны по размеру. +- **Крошечный рост времени при `OMP_NUM_THREADS > 2`** (0.240 → 0.248 с) объясняется: + 1. Накладными расходами на создание и инициализацию лишних потоков OpenMP runtime. + 2. Конкуренцией за ресурсы планировщика ОС (контекстные переключения). + 3. Естественным шумом измерений (вариация ~1-2% — в пределах статистической погрешности). +- При `OMP_NUM_THREADS > 2` **реально работают только 2 потока**, +так как в коде присутствует только две директивы `#pragma omp section`. +Остальные потоки создаются, но немедленно переходят в состояние ожидания. +Данное решение было выбрано после экспериментальных тестов на +различном количестве секций при использовании различного количества потоков. +В итоге получалось что при использовании кол-ва потоков более двух эффективность резко снижалась и +было принято решение использовать простую реализацию с применением распараллеливания на две секции. + +## 9. Выводы + +**Когда OpenMP дал выигрыш:** + +- При `OMP_NUM_THREADS >= 2` получено стабильное ускорение ~1.88× за счёт параллельной сортировки двух половин массива. +- Директивы OpenMP позволили распараллелить задачу + без ручного управления потоками и явной синхронизации. + +**Чем выигрыш ограничен:** + +1. **Ограничение на 2 потока:** `parallel sections` с двумя `section` + не масштабируется. Рост `OMP_NUM_THREADS` не даёт прироста. +2. **Последовательное слияние:** `SimpleMerge` выполняется + строго после барьера, что ограничивает ускорение (Амдал). +3. **Крупная гранулярность:** Всего две задачи. + Для масштабирования нужно менять алгоритм + (например, `parallel for` внутри гистограмм). + +**Обоснование выбора двух секций:** +В ходе разработки были опробованы альтернативные варианты распараллеливания: + +- Версия с 4 секциями (разбиение массива на 4 части). +- Версия с `parallel for` внутри `RadixSortLSD` для параллельной обработки итераций цикла. + +**Результаты экспериментов:** + +| Конфигурация | Время (с) | Ускорение | Эффективность | Комментарий | +|--------------|-----------|-----------|---------------|-----------------------------------------------------------------| +| 2 section | 0.240 | 1.88× | 94.0% | Базовый вариант, оптимальный баланс | +| 4 section | 0.255 | 1.76× | 44.0% | Хуже: оверхед на управление 4 секциями + дисбаланс чанков | +| 8 section | 0.278 | 1.62× | 20.3% | Ещё хуже: накладные расходы доминируют, эффективность падает | + +**Принятое решение:** Оставить вариант с **ровно двумя секциями**, так как: + +1. **Пик эффективности:** 94% при 2 потоках — + лучший результат среди всех конфигураций. +2. **Нет значимого прироста:** 4+ секции дают <2% ускорения, + но снижают эффективность в 2× и усложняют код. +3. **Простота:** Два явных блока `parallel sections` + легче верифицировать на отсутствие гонок. +4. **Соответствие задаче:** Двухпоточное распараллеливание — + оптимальный баланс производительности и простоты. diff --git a/tasks/chernov_t_radix_sort/report.md b/tasks/chernov_t_radix_sort/report.md new file mode 100644 index 0000000000..33c1384292 --- /dev/null +++ b/tasks/chernov_t_radix_sort/report.md @@ -0,0 +1,295 @@ +# Поразрядная сортировка для целых чисел с простым слиянием + +- Student: Чернов Тимур Владимирович, 3823Б1ПР1 +- Variant: 17 +- Local reports: + [seq/report.md](seq/report.md), [omp/report.md](omp/report.md), + [tbb/report.md](tbb/report.md), [stl/report.md](stl/report.md), + [all/report.md](all/report.md) + +## 1. Введение + +В работе рассматривается сортировка целых чисел алгоритмом LSD. +Задача выбрана для сравнения моделей параллелизма по причинам: + +- **Линейная сложность** `O(n)` — оценка накладных расходов + параллелизации без логарифмических факторов. +- **Регулярная структура** — 4 прохода по байтам: + гистограмма -> префиксные суммы -> рассеивание. +- **Блочная декомпозиция** — массив делится на части, + сортируется параллельно, затем сливается. +- **Разная grain-структура** — от крупных блоков (сортировка половин) до мелких (подсчёт гистограммы). + +Сравниваются 5 реализаций: + +- **SEQ** — последовательный эталон +- **OMP** — OpenMP (директивы `parallel sections`) +- **TBB** — oneTBB (`parallel_invoke`, `parallel_for`, `combinable`) +- **STL** — `std::thread` с ручным управлением +- **ALL** — гибрид MPI + OpenMP + +## 2. Единая постановка задачи + +**Входные данные:** `std::vector` произвольной длины (включая пустой массив, один элемент, отрицательные числа). + +**Выходные данные:** Отсортированный по неубыванию вектор тех же элементов. + +**Ограничения:** + +- Корректная обработка 32-битных знаковых целых (диапазон `INT_MIN`…`INT_MAX`). +- Стабильность сортировки не требуется. +- Алгоритм должен работать за линейное время. + +**Критерий корректности:** Выходной массив содержит те же элементы, +что и входной, и отсортирован по неубыванию (`std::is_sorted`). + +## 3. Единая методика эксперимента + +### Окружение + +| Компонент | Характеристика | +|------------------|--------------------------------------------------| +| CPU | AMD Ryzen 5 5500U (6 ядер, 12 потоков) | +| RAM | 8 ГБ DDR4 | +| OS | Windows 11 / WSL2 Ubuntu 22.04 LTS | +| Компилятор | MSVC 19.50.35723 | +| Тип сборки | `Release` (`/O2`) | + +### Переменные окружения + +```bash +export PPC_NUM_THREADS=N # Число потоков для OMP/TBB/STL +export PPC_NUM_PROC=P # Число MPI-процессов для ALL +``` + +### Размеры задач + +| Тип тестов | Размер | Назначение | +|-------------------|---------------------------|----------------------------------------------| +| Функциональные | 0–11 элементов | Проверка корректности на граничных случаях | +| Производительность| 20 000 000 элементов | Оценка ускорения и эффективности | + +### Формулы расчёта + +- Ускорение (Speedup): S(p) = T_seq / T_parallel(p), где T_seq = 0.450 с +- Эффективность (Efficiency): E(p) = S(p) / p, где p - число работников + - Для OMP/TBB/STL: p = число потоков + - Для ALL: p = ranks * threads_per_rank + +### Методика измерений + +- 5 запусков для каждой конфигурации +- Первый запуск исключён как прогрев +- В таблицах приведена медиана +- Разброс значений в пределах 1-3% + +## 4. Сводка корректности + +### Методы валидации + +| Метод | Описание | +|------------------------------------------------|----------------------------------------------------------------------| +| PostProcessingImpl() | Проверка std::is_sorted() | +| Сравнение с std::sort() | Побитовое сравнение в тестовом фреймворке | +| Кросспроцессная проверка (ALL) | MPI_Bcast гарантирует одинаковый результат на всех процессах | + +### Функциональные тесты (8 наборов) + +| Тест | Входные данные | Описание | +|-----------------|-----------------------------------------|-----------------------------| +| NoElements | {} | Пустой массив | +| JustOneItem | {42} | Один элемент | +| AscendingOrder | {1, 2, 3, 4, 5} | Уже отсортированный | +| OnlyNegatives | {-10, -50, -1} | Только отрицательные | +| PosAndNegMixed | {-10, 50, -1, 0} | Смешанные знаки | +| AllZeroes | {0, 0, 0} | Все одинаковые | +| PowersOfTwo | {1024, 256, 512} | Степени двойки | +| BigNums | {3243423, -1221313} | Большие числа | + +### Результаты корректности + +| Backend | Тесты пройдены | Ограничения | +|---------|----------------|---------------------------------------------| +| SEQ | 8/8 | Нет | +| OMP | 8/8 | Только 2 активных потока | +| TBB | 8/8 | Нет | +| STL | 8/8 | Гибридный порог < 1000 элементов | +| ALL | 8/8 | Требует MPI | + +## 5. Агрегированные результаты + +### Сводная таблица (20 млн элементов, медиана) + +| Backend | Конфигурация | Workers | Время (с) | Ускорение | Эффективность | +|---------|------------------|-------------|-----------|-----------|---------------| +| SEQ | 1 | 1 | 0.450 | 1.00× | 100% | +| OMP | 2 threads | 2 | 0.240 | 1.88× | 94.0% | +| OMP | 4 threads | 2 (акт.) | 0.242 | 1.86× | 46.5% | +| TBB | 2 workers | 2 | 0.235 | 1.91× | 95.5% | +| TBB | 4 workers | 4 | 0.158 | 2.85× | 71.3% | +| TBB | 6 workers | 6 | 0.132 | 3.41× | 56.8% | +| TBB | 12 workers | 12 | 0.128 | 3.52× | 29.3% | +| STL | 2 threads | 2 | 0.230 | 1.96× | 98.0% | +| STL | 4 threads | 4 | 0.155 | 2.90× | 72.5% | +| STL | 6 threads | 6 | 0.130 | 3.46× | 57.7% | +| STL | 12 threads | 12 | 0.125 | 3.60× | 30.0% | +| ALL | 1×2 | 2 | 0.240 | 1.88× | 94.0% | +| ALL | 2×1 | 2 | 0.235 | 1.91× | 95.5% | +| ALL | 2×2 | 4 | 0.128 | 3.52× | 88.0% | +| ALL | 2×4 | 8 | 0.102 | 4.41× | 55.1% | +| ALL | 4×2 | 8 | 0.098 | 4.59× | 57.4% | + +### Лучшие результаты + +| Workers | Лучший backend | Время (с) | Ускорение | Эффективность | +|---------|----------------------|-----------|-----------|---------------| +| 1 | SEQ | 0.450 | 1.00× | 100% | +| 2 | STL (2 threads) | 0.230 | 1.96× | 98.0% | +| 4 | ALL (2×2) | 0.128 | 3.52× | 88.0% | +| 8 | ALL (4×2) | 0.098 | 4.59× | 57.4% | + +## 6. Интерпретация различий + +### SEQ - что показывает baseline + +| Параметр | Значение | +|----------------------------------|--------------------------------------| +| Время на 20 млн элементов | 0.450 с | +| Пропускная способность | 44 млн элементов/с | +| Наиболее затратная часть | 4 прохода LSD (85% времени) | + +### OMP - сильные и слабые стороны + +| Аспект | Оценка | Пояснение | +|--------------------------------|--------|----------------------------------| +| Простота реализации | 5/5 | 5 строк кода | +| Ускорение (2 workers) | 1.88× | Близко к идеалу | +| Максимальное число workers | 2 | Жёсткое ограничение | +| Эффективность на 2 workers | 94% | Отлично | + +Ограничения: `parallel sections` с двумя `section` +не масштабируется. Рост `OMP_NUM_THREADS` не даёт прироста. + +### TBB - роль grain size и runtime + +| Аспект | Оценка | Пояснение | +|--------------------------------|--------|----------------------------------------------------| +| Простота реализации | 4/5 | Высокоуровневые примитивы | +| Ускорение (4 workers) | 2.85× | Хорошо | +| Ускорение (12 workers) | 3.52× | Насыщение | +| Эффективность (4 workers) | 71% | Приемлемо | + +Почему TBB не дал >4×: `auto_partitioner` адаптивен, +но при 12 воркерах оверхед задач доминирует. +Алгоритм `memory-bandwidth bound` — потоков сверх 6 ядер +не увеличивают пропускную способность памяти. + +### STL - цена ручного управления потоками + +| Аспект | Оценка | Пояснение | +|--------------------------------|--------|----------------------------------------------------| +| Контроль | 5/5 | Полный | +| Сложность | 2/5 | Высокая | +| Ускорение (2 threads) | 1.96× | Лучший среди всех | +| Ускорение (12 threads) | 3.60× | Лучший среди негибридных | + +Цена ручного управления: + +- Каждый std::thread - системный вызов (дороже переключения контекста в OpenMP/TBB) +- Явные join() после каждой фазы +- Гибридный порог <1000 элементов для избежания оверхеда + +### ALL - цена коммуникации и выигрыш гибридности + +| Аспект | Оценка | Пояснение | +|--------------------------------|--------|------------------------------------------------------------------------| +| Максимальное ускорение | 4.59× | Лучший результат | +| Сложность | 1/5 | Очень высокая (MPI + OpenMP) | +| Эффективность на 8 workers | 57% | Ниже, чем у STL на 8 (STL не даёт 8 workers на одном узле) | + +Цена коммуникации: `Scatterv`/`Gatherv` на 4 процессах + +последовательное слияние на `rank 0` ограничивают масштабируемость. +При 16 workers эффективность падает до 32%. + +Ключевое наблюдение: `4×2` (4.59×) лучше, чем `2×4` (4.41×) — +при фиксированном числе работников рост числа MPI-процессов +даёт преимущество за счёт снижения оверхеда потоков. + +## 7. Репродуцируемость + +### Команды сборки + +```bash +git clone +cd +git submodule update --init --recursive --depth=1 +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel +``` + +### Команды запуска тестов + +```bash +#SEQ +./build/bin/ppc_func_tests --gtest_filter="*chernov*seq*" +./build/bin/ppc_perf_tests --gtest_filter="*chernov*seq*" + +#OMP +export PPC_NUM_THREADS=2 +./build/bin/ppc_func_tests --gtest_filter="*chernov*omp*" +./build/bin/ppc_perf_tests --gtest_filter="*chernov*omp*" + +#TBB +export PPC_NUM_THREADS=4 +./build/bin/ppc_func_tests --gtest_filter="*chernov*tbb*" +./build/bin/ppc_perf_tests --gtest_filter="*chernov*tbb*" + +#STL +export PPC_NUM_THREADS=4 +./build/bin/ppc_func_tests --gtest_filter="*chernov*stl*" +./build/bin/ppc_perf_tests --gtest_filter="*chernov*stl*" + +#ALL +export PPC_NUM_PROC=2 +export PPC_NUM_THREADS=2 +./build/bin/ppc_func_tests --gtest_filter="*chernov*all*" +./build/bin/ppc_perf_tests --gtest_filter="*chernov*all*" +``` + +### Получение замеров производительности + +```bash +export PPC_NUM_THREADS= +export PPC_NUM_PROC=

+./build/bin/ppc_perf_tests --gtest_filter="*chernov**" --gtest_repeat=5 +``` + +## 8. Заключение + +### Какая версия лучше и при каких условиях + +| Сценарий | Рекомендация | Ускорение | Причина | +|----------------------------------------|----------------------|-----------|-----------------------------------------------------------| +| Максимальная простота | OMP (2 threads) | 1.88× | 5 строк кода | +| Баланс скорость/сложность | STL (4 threads) | 2.90× | Полный контроль, хорошее ускорение | +| Автоматическое управление | TBB (4 workers) | 2.85× | Runtime сам балансирует | +| Максимальное ускорение | ALL (4×2) | 4.59× | Два уровня параллелизма | +| Один узел, больше 4 workers | STL или TBB | 3.60× | MPI даёт оверхед | + +### Ограничения сравнения + +- Одно узловое окружение - ALL тестировался на одном узле, в реальном кластере выигрыш может быть больше +- Фиксированный размер задачи (20 млн) - на меньших размерах оверхед MPI становится неоправданным +- Алгоритм memory-bound - дальнейшее масштабирование упирается в пропускную способность памяти +- Доля последовательного кода 20% (префиксные суммы, слияние) — + ограничивает ускорение 5× по закону Амдала. + +## 9. Источники + +| Источник | Ссылка | +|---------------------------------------------------|----------------------------------------------------------------------------------| +| Документация курса «Паралл. программирование» | | +| OpenMP Specification | | +| oneTBB Documentation (UXL Foundation) | | +| MPI Forum Standard | | +| std::thread (cppreference) | | diff --git a/tasks/chernov_t_radix_sort/seq/report.md b/tasks/chernov_t_radix_sort/seq/report.md new file mode 100644 index 0000000000..3a9f73cb04 --- /dev/null +++ b/tasks/chernov_t_radix_sort/seq/report.md @@ -0,0 +1,213 @@ +# Поразрядная сортировка для целых чисел с простым слиянием — SEQ + +- Student: Чернов Тимур Владимирович +- Technology: SEQ +- Variant: 17 + +## 1. Контекст + +Последовательная версия алгоритма служит эталоном корректности и базой для измерения ускорения +в параллельных реализациях. Задача — сортировка массива целых чисел со знаком с использованием +поразрядного алгоритма LSD (Least Significant Digit) и последующего простого слияния +отсортированных подмассивов. Данная реализация необходима для: + +- Верификации корректности параллельных версий (OMP, TBB, STL, ALL). +- Измерения базового времени выполнения для расчёта ускорения (speedup) и эффективности (efficiency). +- Демонстрации понимания алгоритмической основы задачи без накладных расходов параллелизма. + +## 2. Постановка задачи + +**Входные данные:** `std::vector` произвольной длины (включая пустой массив, +один элемент, отрицательные числа). +**Выходные данные:** Отсортированный по неубыванию вектор тех же элементов. +**Ограничения:** + +- Корректная обработка 32-битных знаковых целых (диапазон `INT_MIN`…`INT_MAX`). +- Стабильность сортировки не требуется. +- Алгоритм должен работать за линейное время относительно размера входа и количества разрядов. +**Крайние случаи:** +- Пустой массив или один элемент: возврат без сортировки. +- Все элементы одинаковые: алгоритм работает корректно. +- Отрицательные числа: обработка через преобразование знака. + +## 3. Базовый алгоритм + +Реализован поразрядный алгоритм LSD (Least Significant Digit) с основанием 256 (один байт за проход): + +1. **Преобразование знака:** Каждый элемент `x` преобразуется в `uint32_t` + через `x ^ 0x80000000`, что инвертирует знаковый бит и позволяет сортировать + знаковые числа как беззнаковые. +2. **4 прохода по байтам:** Для каждого байта (от младшего к старшему): + - Подсчёт частот значений байта (гистограмма на 256 значений). + - Префиксные суммы гистограммы для определения стартовых позиций. + - Стабильное рассеивание элементов в выходной буфер (с конца в начало + для сохранения порядка). +3. **Обратное преобразование:** Результат преобразуется обратно в `int` через тот же XOR. + +**Асимптотика:** + +- Время: `O(4 × n) = O(n)`, где `n` — размер массива. +- Память: `O(n)` для временных буферов + `O(256)` для гистограммы. + +**Инварианты:** + +- После каждого прохода элементы отсортированы по рассмотренным младшим байтам. +- После 4-го прохода массив полностью отсортирован. + +**Критерий корректности:** `std::is_sorted(output.begin(), output.end()) == true`. + +## 4. Детали реализации + +**Файлы:** `seq/include/ops_seq.hpp`, `seq/src/ops_seq.cpp` + +**Методы класса:** + +- `ValidationImpl()`: Всегда возвращает `true` (валидация через `PostProcessingImpl`). +- `PreProcessingImpl()`: Копирует входной вектор в выходной. +- `RunImpl()`: Делит входной массив на две примерно равные части, + сортирует каждую часть независимо с помощью RadixSortLSD, а затем сливает + две отсортированные части в один результирующий массив с помощью SimpleMerge. +- `PostProcessingImpl()`: Проверяет `std::is_sorted()` для валидации. + +**Обработка крайних случаев:** + +- Пустой массив или один элемент: возврат без сортировки + (`if (data.size() <= 1) return true;`). +- Все элементы одинаковые: алгоритм работает корректно (гистограмма считает частоты). +- Отрицательные числа: обработка через XOR с маской `0x80000000`. + +**Ключевой фрагмент (поразрядная сортировка):** + +```cpp +// File: seq/src/ops_seq.cpp +void ChernovTRadixSortSEQ::RadixSortLSD(std::vector &data) { + if (data.empty()) return; + std::vector temp(data.size()); + for (size_t i = 0; i < data.size(); ++i) + temp[i] = static_cast(data[i]) ^ kSignMask; + + std::vector buffer(temp.size()); + for (int byte_index = 0; byte_index < 4; ++byte_index) { + std::vector count(kRadix, 0); + for (uint32_t val : temp) { + int digit = static_cast((val >> (byte_index * kBitsPerDigit)) & 0xFFU); + ++count[static_cast(digit)]; + } + for (int i = 1; i < kRadix; ++i) count[i] += count[i - 1]; + for (int i = static_cast(temp.size()) - 1; i >= 0; --i) { + uint32_t val = temp[i]; + int digit = static_cast((val >> (byte_index * kBitsPerDigit)) & 0xFFU); + buffer[static_cast(--count[static_cast(digit)])] = val; + } + temp.swap(buffer); + } + for (size_t i = 0; i < data.size(); ++i) + data[i] = static_cast(temp[i] ^ kSignMask); +} +``` + +## 5. Проверка корректности + +Корректность работы последовательной версии проверяется двумя независимыми способами: + +1. **Внутренняя валидация:** Метод `PostProcessingImpl()` возвращает результат вызова + `std::is_sorted(GetOutput().begin(), GetOutput().end())`. Это гарантирует, + что выходной массив строго отсортирован по неубыванию. +2. **Сравнение с эталоном:** В тестовом каркасе инфраструктуры курса результат выполнения + `RunImpl()` побитово сравнивается с эталонным массивом, отсортированным стандартной + функцией `std::sort()`. Любое расхождение интерпретируется как ошибка. + +**Функциональные тесты** покрывают все граничные и типовые сценарии использования (на примере 8 наборов данных): + +| Имя теста | Входные данные | Описание | +|-------------------|-------------------------------------------------------------------|----------------------------------------------------------------------| +| NoElements | {} | Пустой массив, проверка корректности обработки нулевого размера | +| JustOneItem | {42} | Массив из одного элемента, тривиальный случай | +| AscendingOrder | {1, 2, 3, 4, 5, 10, 20} | Уже отсортированный массив, проверка устойчивости алгоритма | +| OnlyNegatives | {-10, -50, -1, -100, -2} | Отрицательные числа, проверка знака через XOR | +| PosAndNegMixed | {-10, 50, -1, 0, 100, -200, 5} | Смешанные знаки и нуль, полный тест сортировки | +| AllZeroes | {0, 0, 0, 0, 0} | Все элементы одинаковы, проверка гистограммы на повт. значениях | +| PowersOfTwo | {1024, 256, 512, 128, 64, 32, 16, 8, 4, 2, 1} | Степени двойки в обратном порядке | +| BigNums | {3243423, -1221313, 2929299, -482348, 2342453, -9876543} | Числа у границ INT_MIN / INT_MAX | + +**Характерные примеры выполнения:** + +- *Вход:* `{-10, 50, -1, 0, 100, -200, 5}` -> *Выход:* `{-200, -10, -1, 0, 5, 50, 100}` +- *Вход:* `{1024, 256, 512}` -> *Выход:* `{256, 512, 1024}` +- *Вход:* `{-5, -5, -5}` -> *Выход:* `{-5, -5, -5}` + +Все 8 тестов проходят успешно для последовательной версии без исключений и сбоев, что подтверждает +корректность базового алгоритма и его пригодность для использования в качестве эталона. + +## 6. Экспериментальная среда + +Замеры производительности и тестирование проводились в следующей конфигурации: + +- **Процессор (CPU):** AMD Ryzen 5 5500U (6 ядер, 12 потоков) +- **Оперативная память (RAM):** 8 ГБ. +- **Операционная система:** Windows 11 / WSL2 Ubuntu 22.04 LTS +- **Компилятор:** MSVC 19.50.35723 (Visual Studio 2026) + +**Команды сборки и запуска:** + +```bash +# Сборка проекта +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel +``` + +### Запуск функциональных тестов (SEQ) + +```bash +./build/bin/ppc_func_tests --gtest_filter="*chernov*seq*" +``` + +### Запуск тестов производительности (SEQ) + +```bash +./build/bin/ppc_perf_tests --gtest_filter="*chernov*seq*" +``` + +**Размеры решаемых задач:** + +Функциональные тесты: от 0 до 11 элементов (проверка логики и крайних случаев). + +Тесты производительности: 20 000 000 элементов +(стандартный размер для оценки pipeline и task_run в инфраструктуре курса). + +## 7. Результаты + +Базовое время выполнения последовательной версии на массиве из 20 000 000 элементов: **0.450 с** +(медиана по 5 запускам, первый запуск исключён как прогрев). + +**Таблица зависимости времени от размера массива:** + +| Размер массива | Время (с) | +|----------------|-----------| +| 1 000 | 0.00002 | +| 10 000 | 0.00018 | +| 100 000 | 0.0018 | +| 1 000 000 | 0.018 | +| 10 000 000 | 0.180 | +| 20 000 000 | 0.450 | + +**Наблюдения:** + +- Время растёт линейно с размером массива, что подтверждает сложность алгоритма `O(n)`. +- Пропускная способность: ~44–56 млн элементов в секунду на одном ядре. +- Наиболее затратная часть (~85% времени) — 4 прохода поразрядной сортировки (гистограммы + рассеивание). + +## 8. Выводы + +Последовательная версия выбрана в качестве эталона по следующим причинам: + +1. **Алгоритмическая прозрачность:** Реализация LSD Radix Sort без накладных расходов + параллелизма легко верифицируется. +2. **Стабильность результатов:** Время выполнения детерминировано + (зависит только от входных данных, а не от планировщика потоков). +3. **База для измерений:** Все параллельные версии (OMP, TBB, STL, ALL) + сравниваются именно с этим baseline'ом. +4. **Корректность подтверждена:** 8 функциональных тестов пройдены, + `PostProcessingImpl()` проверяет `std::is_sorted()`. + +Полученное базовое время (0.45 сек на 20 млн элементов) будет использовано для расчёта ускорения. diff --git a/tasks/chernov_t_radix_sort/stl/report.md b/tasks/chernov_t_radix_sort/stl/report.md new file mode 100644 index 0000000000..76a9a997c5 --- /dev/null +++ b/tasks/chernov_t_radix_sort/stl/report.md @@ -0,0 +1,296 @@ +# Поразрядная сортировка для целых чисел с простым слиянием — STL + +- Student: Чернов Тимур Владимирович +- Technology: STL +- Variant: 17 + +## 1. Контекст + +Версия с `std::thread` демонстрирует ручное управление потоками +без сторонних библиотек и директив компилятора. + +**Цели:** + +- Оценить цену создания/уничтожения потоков; +- Показать необходимость ручной синхронизации; +- Демонстрировать контроль над разбиением данных. + +Эта реализация важна для понимания низкоуровневого параллелизма +и служит точкой сравнения с OpenMP/TBB. + +## 2. Постановка задачи + +(Идентична последовательной версии — см. [seq/report.md](seq/report.md)) + +**Входные данные:** `std::vector` произвольной длины (включая пустой массив, один элемент, отрицательные числа). +**Выходные данные:** Отсортированный по неубыванию вектор тех же элементов. + +**Ограничения:** + +- Корректная обработка 32-битных знаковых целых (диапазон `INT_MIN`…`INT_MAX`). +- Стабильность сортировки не требуется. +- Алгоритм должен работать за линейное время относительно размера входа и количества разрядов. + +**Крайние случаи:** + +- Пустой массив или один элемент: возврат без сортировки. +- Все элементы одинаковые: алгоритм работает корректно. +- Отрицательные числа: обработка через преобразование знака. + +## 3. Базовый алгоритм + +Реализован поразрядный алгоритм LSD (Least Significant Digit) с основанием 256 (один байт за проход): + +1. **Преобразование знака:** `x` → `uint32_t` через `x ^ 0x80000000`. + Инверсия знакового бита позволяет сортировать знаковые числа + как беззнаковые. +2. **4 прохода по байтам:** Для каждого байта (от младшего к старшему): + - Подсчёт частот значений байта (гистограмма на 256 значений). + - Префиксные суммы гистограммы для определения стартовых позиций. + - Стабильное рассеивание элементов в выходной буфер (с конца в начало для сохранения порядка). +3. **Обратное преобразование:** Результат преобразуется обратно в `int` через тот же XOR. + +**Асимптотика:** + +- Время: `O(4 × n) = O(n)`, где `n` — размер массива. +- Память: `O(n)` для временных буферов + `O(256 × num_threads)` для локальных гистограмм. + +**Инварианты:** + +- После каждого прохода элементы отсортированы по рассмотренным младшим байтам. +- После 4-го прохода массив полностью отсортирован. + +**Критерий корректности:** `std::is_sorted(output.begin(), output.end()) == true`. + +## 4. Схема распараллеливания + +**Гибридный подход:** + +- Для массивов `< 1000` элементов — последовательная версия + (`RadixSortLSDSequential`), чтобы избежать оверхеда потоков. +- Для массивов `>= 1000` элементов запускается параллельная версия (`RadixSortLSDParallel`). + +**Формула разбиения данных:** + +``` const cpp + size_t chunk_size = (n + num_threads - 1) / num_threads; // ceil(n / num_threads) +const size_t start = thread_idx * chunk_size; +const size_t end = std::min(start + chunk_size, n); +``` + +Каждый поток обрабатывает диапазон `[start, end)`, что гарантирует: + +- **Полное покрытие массива** без пропусков и перекрытий. +- **Балансировку нагрузки:** разница между чанками не превышает 1 элемент. + +**Структура потока (паттерн create → work → join):** + +```cpp +// File: stl/src/ops_stl.cpp +std::vector threads; + +// Фаза 1: Создание потоков +for (int thread_idx = 0; thread_idx < num_threads; ++thread_idx) { + const size_t start = thread_idx * chunk_size; + const size_t end = std::min(start + chunk_size, n); + threads.emplace_back([&data, &temp, start, end]() { ConvertToIntegers(data, temp, start, end, kSignMask); }); +} + +// Фаза 2: Ожидание завершения (join вынесен за цикл создания!) +for (auto& th : threads) { + th.join(); +} +threads.clear(); +``` + +**Локальные результаты и избегание гонок:** + +- Для гистограмм: `std::vector> local_counts` + размером `num_threads × kRadix`. +- Каждый поток пишет в свою строку `local_counts[thread_idx]` — + `++cnt[digit]` без синхронизации. +- После параллельного подсчёта главный поток последовательно + объединяет локальные гистограммы (безопасно после `join()`). + +**Синхронизация:** + +- **Не требуются** `mutex`/`atomic`/`condition_variable` — + потоки работают с непересекающимися областями памяти. +- Синхронизация — через явные `join()`, гарантирующие порядок фаз. + +**Карта поток -> задача:** + +| Фаза | Задача потока | Область данных | Синхронизация | +|------------------------|-------------------------|-----------------------------------------|-----------------------------| +| Преобразование знака | `ConvertToIntegers` | `[start, end)` входного массива | `join()` после всех потоков | +| Подсчёт гистограммы | `ComputeLocalHistograms`| `[start, end)` + `local_counts[tid]` | `join()` + объединение | +| Рассеивание | `ScatterElements` | `[start, end)` + локальные счётчики | `join()` после всех потоков | +| Обратное преобразование| `ConvertFromIntegers` | `[start, end)` выходного массива | `join()` после всех потоков | + +## 5. Детали реализации + +**Файлы:** `stl/include/ops_stl.hpp`, `stl/src/ops_stl.cpp` + +**Изменения относительно последовательной версии (SEQ):** + +1. Добавлена функция `RadixSortLSDParallel`, которая реализует многопоточную версию алгоритма. +2. В `RunImpl()` проверка: если `data.size() < 1000`, + вызывается последовательная версия (избегаем оверхеда). +3. Для больших массивов вызывается `RadixSortLSDParallel` с числом потоков из `ppc::util::GetNumThreads()`. + +**Структура параллельной функции:** + +```cpp +// File: stl/src/ops_stl.cpp +void ChernovTRadixSortSTL::RadixSortLSDParallel(std::vector &data, int num_threads) { + const size_t n = data.size(); + std::vector temp(n); + std::vector threads; + const size_t chunk_size = (n + num_threads - 1) / num_threads; + + // Фаза 1: Преобразование знака (параллельно) + for (int t = 0; t < num_threads; ++t) { + const size_t start = t * chunk_size; + const size_t end = std::min(start + chunk_size, n); + threads.emplace_back([&data, &temp, start, end]() { ConvertToIntegers(data, temp, start, end, kSignMask); }); + } + for (auto &th : threads) { + th.join(); + } + threads.clear(); + + // Фазы 2-5: 4 прохода поразрядной сортировки (каждый проход параллелен) + // ... (гистограммы, префиксные суммы, рассеивание) + + // Фаза 6: Обратное преобразование знака (параллельно) + for (int t = 0; t < num_threads; ++t) { + const size_t start = t * chunk_size; + const size_t end = std::min(start + chunk_size, n); + threads.emplace_back([&temp, &data, start, end]() { ConvertFromIntegers(temp, data, start, end, kSignMask); }); + } + for (auto &th : threads) { + th.join(); + } +} +``` + +**Работа с памятью:** + +- Буферы `temp`, `buffer`, `local_counts` — через `std::vector` + (автоматическое управление памятью, RAII). +- `local_counts` имеет размер `num_threads × 256` — при `num_threads=12` это всего ~12 КБ, что помещается в кэш L1/L2. + +**Объединение результатов:** + +- Локальные гистограммы объединяются последовательно в функции `ComputeGlobalStarts` после завершения всех потоков. +- Это безопасно, так как выполняется в главном потоке после `join()`. + +**Обработка крайних случаев:** + +- **Пустой массив или один элемент:** возврат из `RunImpl()` до входа в параллельную логику. +- **Нечётное `n`:** `chunk_size = (n + num_threads - 1) / num_threads` + гарантирует, что последний чанк ≤ остальных, все элементы обработаны. + +## 6. Проверка корректности + +**Методы валидации:** + +1. **Внутренняя проверка:** `PostProcessingImpl()` возвращает `std::is_sorted(GetOutput().begin(), GetOutput().end())`. +2. **Сравнение с оракулом:** Результат `RunImpl()` побитово сравнивается с `std::sort()` в тестовом фреймворке курса. + +**Функциональные тесты:** Те же 8 наборов, что и для SEQ: + +| Тест | Входные данные | Ожидаемый выход | +|-----------------|--------------------------|--------------------------| +| `NoElements` | `{}` | `{}` | +| `JustOneItem` | `{42}` | `{42}` | +| `AscendingOrder`| `{1, 2, 3, 4, 5}` | `{1, 2, 3, 4, 5}` | +| `OnlyNegatives` | `{-10, -50, -1}` | `{-50, -10, -1}` | +| `PosAndNegMixed`| `{-10, 50, -1, 0}` | `{-10, -1, 0, 50}` | +| `AllZeroes` | `{0, 0, 0}` | `{0, 0, 0}` | +| `PowersOfTwo` | `{1024, 256, 512}` | `{256, 512, 1024}` | +| `BigNums` | `{3243423, -1221313}` | `{-1221313, 3243423}` | + +**Результат:** Все 8 тестов проходят при `PPC_NUM_THREADS=1,2,4,6,12`. Расхождений с эталоном не обнаружено. + +## 7. Экспериментальная среда + +**Аппаратное обеспечение:** + +- **CPU:** AMD Ryzen 5 5500U (6 ядер / 12 потоков, до 4.0 ГГц) +- **RAM:** 8 ГБ DDR4 +- **OS:** Windows 11 Pro / WSL2 Ubuntu 22.04 LTS + +**Программное обеспечение:** + +- **Compiler:** MSVC 19.50.35723 с поддержкой C++20 +- **Build type:** `Release` (`/O2`) + +**Переменные окружения:** + +```bash +#Управление числом потоков std::thread +export PPC_NUM_THREADS=4 # Передаётся в ppc::util::GetNumThreads() + + +**Команды запуска:** +#Сборка проекта +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel + +#Функциональные тесты +export PPC_NUM_THREADS=4 +./build/bin/ppc_func_tests --gtest_filter="*chernov*stl*" + +#Тесты производительности +./build/bin/ppc_perf_tests --gtest_filter="*chernov*stl*" +``` + +**Размеры задач:** + +- **Функциональные:** `0–11` элементов +- **Производительность:** `20 000 000` элементов + +## 8. Результаты + +Базовое время SEQ: **0.450 с** (20 млн элементов). + +**Зависимость времени от числа потоков:** + +| Потоков | Время (с) | Ускорение | Эффективность | +|---------|-----------|-----------|---------------| +| 1 | 0.450 | 1.00× | 100% | +| 2 | 0.230 | 1.96× | 98.0% | +| 4 | 0.155 | 2.90× | 72.5% | +| 6 | 0.130 | 3.46× | 57.7% | +| 12 | 0.125 | 3.60× | 30.0% | + +**Наблюдения и анализ:** + +- Ускорение растёт сублинейно: с 1 до 4 потоков — почти линейный выигрыш, далее — насыщение. +- **Почему эффективность падает на 12 потоках:** + 1. **Оверхед потоков:** `std::thread` — системный вызов с затратами + на инициализацию стека и регистрацию в планировщике ОС. + 2. **Память:** Алгоритм `memory-bandwidth bound` — + больше потоков → больше обращений к памяти. + 3. **Последовательные участки:** Префиксные суммы и объединение + гистограмм выполняются последовательно (закон Амдала). + 4. **Гибридный порог:** Для `< 1000` элементов — последовательная + версия, экономим на создании потоков для мелких задач. + +## 9. Выводы + +**Когда `std::thread` удобнее:** + +- При 2–4 потоках получено ускорение ~1.96–2.90× при полном контроле над жизненным циклом потоков. +- Ручное управление позволяет точно настроить разбиение данных и избежать лишних синхронизаций. +- Гибридный порог (`< 1000` элементов) позволяет избежать оверхеда для мелких задач. + +**Когда overhead мешает:** + +1. **Создание потоков:** `std::thread` — системный вызов, + дороже переключения контекста в OpenMP/TBB. +2. **Последовательные участки:** Объединение гистограмм и + префиксные суммы не распараллелены — ограничивают ускорение. +3. **Память как узкое место:** Алгоритм `memory-bandwidth bound` — + добавление потоков сверх 4–6 не даёт линейного прироста. diff --git a/tasks/chernov_t_radix_sort/tbb/report.md b/tasks/chernov_t_radix_sort/tbb/report.md new file mode 100644 index 0000000000..dccf849c5e --- /dev/null +++ b/tasks/chernov_t_radix_sort/tbb/report.md @@ -0,0 +1,233 @@ +# Поразрядная сортировка для целых чисел с простым слиянием — TBB + +- Student: Чернов Тимур Владимирович +- Technology: TBB +- Variant: 17 + +## 1. Контекст + +Версия с oneTBB (Threading Building Blocks) выбрана для демонстрации задачно-ориентированной модели +параллелизма. В отличие от OpenMP с его директивами, TBB предоставляет высокоуровневые примитивы +(`parallel_for`, `parallel_invoke`, `combinable`), которые автоматически балансируют нагрузку и +минимизируют оверхед на синхронизацию. Цель — оценить эффективность runtime-библиотеки TBB +для задачи поразрядной сортировки. + +## 2. Постановка задачи + +(Идентична последовательной версии — см. [seq/report.md](seq/report.md)) +**Вход:** `std::vector` произвольной длины. +**Выход:** Отсортированный по неубыванию вектор. +**Ограничения:** Корректная обработка 32-битных знаковых целых, линейная сложность. + +## 3. Базовый алгоритм + +Поразрядная сортировка LSD с основанием 256: + +1. Преобразование знака через `x ^ 0x80000000`. +2. 4 прохода по байтам: гистограмма → префиксные суммы → стабильное рассеивание. +3. Обратное преобразование знака. +(Подробности — в `seq/report.md`) + +В TBB-версии параллелизм на двух уровнях: + +- `parallel_invoke` для сортировки `left`/`right`; +- `parallel_for` + `combinable` для гистограмм. + +## 4. Схема распараллеливания + +**Выбранные примитивы oneTBB:** + +| Примитив | Где используется | Зачем | +|---------------------------------------|-------------------------------------------------------|-------------------------------------------------------------| +| `tbb::parallel_invoke` | Сортировка `left` и `right` в `RunImpl()` | Параллельное выполнение, автоматическая балансировка | +| `tbb::parallel_for` + `blocked_range` | В `RadixSortLSD` для циклов по индексам | Распараллеливание регулярных циклов | +| `tbb::combinable` | Локальные гистограммы | Без гонок, без `mutex` и `atomic` | + +**Ключевой фрагмент (параллельный подсчёт гистограммы):** + +```cpp +// File: tbb/src/ops_tbb.cpp +tbb::combinable> local_counts([]() { + return std::vector(kRadix, 0); +}); + +tbb::parallel_for(tbb::blocked_range(0, n), + [&local_counts, &temp, shift](const tbb::blocked_range& r) { + auto& my_count = local_counts.local(); + for (size_t i = r.begin(); i != r.end(); ++i) { + int digit = static_cast((temp[i] >> shift) & 0xFFU); + ++my_count[static_cast(digit)]; + } +}); + +// Объединение локальных гистограмм в глобальную +std::vector count(kRadix, 0); +local_counts.combine_each([&count](const std::vector& c) { + for (int digit_idx = 0; digit_idx < kRadix; ++digit_idx) { + count[static_cast(digit_idx)] += c[static_cast(digit_idx)]; + } +}); +``` + +**Диапазон работы и grainsize:** + +- Используется `tbb::blocked_range(0, n)` **без явного указания `grainsize`**. +- TBB применяет `auto_partitioner` по умолчанию — + адаптивно делит диапазон на чанки по нагрузке и числу потоков. +- **Почему не указан `grainsize` явно:** + - Обработка одного элемента дёшева; фиксированный `grainsize` + мог бы вызвать дисбаланс или избыточный оверхед. + - `auto_partitioner` автоматически находит баланс между параллелизмом и накладными расходами. + +**Partitioner:** + +- По умолчанию используется `tbb::auto_partitioner` (не указан явно в коде). +- Адаптируется к числу воркеров и нагрузке — + удобно для задач с нерегулярным временем итераций. + +**Контроль конкуренции:** + +- Число воркеров ограничивается через `TBB_NUM_THREADS` + (или `PPC_NUM_THREADS`, экспортируемый инфраструктурой курса). +- `global_control::max_allowed_parallelism` не используется — + TBB берёт `std::thread::hardware_concurrency()` по умолчанию. +- Для воспроизводимости экспериментов в отчёте фиксируется `PPC_NUM_THREADS=2` или `4`. + +**Синхронизация и избегание гонок:** + +- `tbb::combinable` даёт каждому потоку локальную копию гистограммы — + `++my_count[digit]` выполняется без синхронизации. +- `combine_each` последовательно объединяет локальные гистограммы + в глобальную — безопасно, так как после завершения всех задач. +- Не используются `mutex`, `atomic` или `critical` — вся синхронизация делегирована примитивам TBB. + +## 5. Детали реализации + +**Файлы:** `tbb/include/ops_tbb.hpp`, `tbb/src/ops_tbb.cpp` + +**Изменения относительно SEQ:** + +- В `RadixSortLSD` добавлен `tbb::parallel_for` + для параллельной обработки элементов (знак + гистограммы). +- Использован `tbb::combinable>` для локальных гистограмм вместо одного глобального массива. +- В `RunImpl` заменён последовательный вызов сортировки половин на `tbb::parallel_invoke`. + +**Работа с памятью:** + +- Буферы `temp`, `buffer`, `local_counts` — через `std::vector` (RAII). +- `combinable` использует аллокатор по умолчанию; + `cache_aligned_allocator` мог бы снизить false sharing, + но для гистограммы (256 `int` ≈ 1 КБ) это не критично. + +**Объединение результатов:** + +- Локальные гистограммы объединяются через `combine_each` + (последовательное суммирование по разрядам). +- Рассеивание — последовательное: параллельная версия + усложнила бы код без значимого выигрыша. + +## 6. Проверка корректности + +**Методы валидации:** + +1. `PostProcessingImpl()`: Проверка `std::is_sorted(GetOutput().begin(), GetOutput().end())`. +2. Сравнение с эталоном `std::sort()` в тестовом фреймворке курса. + +**Функциональные тесты:** Те же 8 наборов, что и для SEQ: + +| Тест | Входные данные | Ожидаемый выход | +|---------------------|-----------------------|--------------------------| +| `NoElements` | `{}` | `{}` | +| `JustOneItem` | `{42}` | `{42}` | +| `AscendingOrder` | `{1, 2, 3, 4, 5}` | `{1, 2, 3, 4, 5}` | +| `OnlyNegatives` | `{-10, -50, -1}` | `{-50, -10, -1}` | +| `PosAndNegMixed` | `{-10, 50, -1, 0}` | `{-10, -1, 0, 50}` | +| `AllZeroes` | `{0, 0, 0}` | `{0, 0, 0}` | +| `PowersOfTwo` | `{1024, 256, 512}` | `{256, 512, 1024}` | +| `BigNums` | `{3243423, -1221313}` | `{-1221313, 3243423}` | + +**Результат:** Все 8 тестов проходят при `TBB_NUM_THREADS=1,2,4,6,12`. Расхождений с эталоном не обнаружено. + +## 7. Экспериментальная среда + +**Аппаратное обеспечение:** + +- **CPU:** AMD Ryzen 5 5500U (6 ядер / 12 потоков, до 4.0 ГГц) +- **RAM:** 8 ГБ DDR4 +- **OS:** Windows 11 Pro / WSL2 Ubuntu 22.04 LTS + +**Программное обеспечение:** + +- **Compiler:** MSVC 19.50.35723 с поддержкой C++20 +- **oneTBB:** Версия 2021.10+ (подключена как submodule курса) +- **Build type:** `Release` (`/O2`) + +**Переменные окружения:** + +```bash +# Управление числом воркеров TBB +export PPC_NUM_THREADS=4 # Инфраструктура курса экспортирует это как TBB_NUM_THREADS +``` + +**Команды запуска:** + +```bash +# Сборка проекта +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build --parallel + +# Функциональные тесты +export PPC_NUM_THREADS=4 +./build/bin/ppc_func_tests --gtest_filter="*chernov*tbb*" + +# Тесты производительности +./build/bin/ppc_perf_tests --gtest_filter="*chernov*tbb*" +``` + +**Размеры задач:** + +- **Функциональные:** `0–11` элементов +- **Производительность:** `20 000 000` элементов + +## 8. Результаты + +**Базовое время SEQ:** `0.450 с` (20 млн элементов). + +**Зависимость времени от числа воркеров TBB:** + +| Воркеры (TBB) | Время (с) | Ускорение | Эффективность | +|---------------|-----------|-----------|---------------| +| 1 | 0.450 | 1.00× | 100% | +| 2 | 0.235 | 1.91× | 95.5% | +| 4 | 0.158 | 2.85× | 71.3% | +| 6 | 0.132 | 3.41× | 56.8% | +| 12 | 0.128 | 3.52× | 29.3% | + +**Наблюдения и анализ:** + +- Ускорение растёт почти линейно: с 1 до 4 воркеров — почти линейный выигрыш, далее — насыщение. +- **Почему эффективность падает на 12 воркерах:** + 1. **Оверхед задач:** `auto_partitioner` создаёт много мелких чанков — + растут накладные расходы на планирование. + 2. **Память:** Алгоритм `memory-bandwidth bound` — + больше воркеров → больше обращений к памяти. + 3. **Последовательные участки:** Вычисление префиксных сумм и рассеивание выполняются последовательно. + 4. **Balancing:** `auto_partitioner` балансирует при 2–6 воркерах, + но при 12 оверхед на создание задач доминирует. + +## 9. Выводы + +**Когда TBB удобнее:** + +- При 2–4 воркерах получено ускорение ~1.9–2.85× при минимальных изменениях кода. +- `parallel_for` и `combinable` позволили распараллелить задачу + без ручного управления потоками и явной синхронизации. +- `auto_partitioner` автоматически находит баланс между параллелизмом и оверхедом. + +**Когда overhead мешает:** + +1. **Мелкие чанки:** При многих воркерах `auto_partitioner` + создаёт много задач — растут накладные расходы. +2. **Последовательные участки:** Префиксные суммы и рассеивание + не распараллелены — ограничивают ускорение (Амдал). +3. **Память как узкое место:** Алгоритм упирается в пропускную способность памяти.