|
| 1 | +# Подсчет числа слов в строке |
| 2 | + |
| 3 | +- **Студент:** Волков Алексей Иванович, 3823Б1ФИ2 |
| 4 | +- **Технология:** SEQ, MPI |
| 5 | +- **Вариант:** 24 |
| 6 | + |
| 7 | +## 1. Введение |
| 8 | + |
| 9 | +Подсчет слов является фундаментальной задачей при обработке текстов. При работе с очень большими текстовыми объемами последовательное выполнение этой задачи может стать узким местом. Целью данной работы является разработка параллельного алгоритма для подсчета слов с использованием технологии MPI (Message Passing Interface) и сравнение его производительности с эталонной последовательной реализацией. |
| 10 | + |
| 11 | +## 2. Постановка задачи |
| 12 | + |
| 13 | +**Задача:** Необходимо подсчитать общее количество "слов" в заданной входной строке. |
| 14 | + |
| 15 | +- **Входные данные:** строка символов `std::string`, |
| 16 | +- **Выходные данные:** целое число `int`, представляющее общее количество слов, |
| 17 | +- **Определение слова:** "словом" считается непрерывная последовательность символов, которая включает: |
| 18 | + - буквы латинского алфавита в верхнем и нижнем регистре (`a-z`, `A-Z`); |
| 19 | + - цифры (`0-9`); |
| 20 | + - символы дефиса (`-`) и подчеркивания (`_`). |
| 21 | +- **Ограничения:** слова разделяются одним или несколькими символами-разделителями, которые не входят в определение "слова". Входная строка может быть пустой, может не содержать слов, а также может начинаться или заканчиваться разделителями. |
| 22 | + |
| 23 | +## 3. Описание алгоритма (базового/последовательного) |
| 24 | + |
| 25 | +Для последовательной реализации был выбран однопроходный алгоритм, основанный на концепции конечного автомата (машины состояний). Алгоритм проходит по строке символ за символом, отслеживая одно из двух состояний: |
| 26 | + |
| 27 | +1. `IN_WORD` — текущий символ является частью слова. |
| 28 | +2. `IN_SEPARATOR` — текущий символ является разделителем. |
| 29 | + |
| 30 | +Логика работы следующая: |
| 31 | +- изначально автомат находится в состоянии `IN_SEPARATOR`; |
| 32 | +- при переходе из состояния `IN_SEPARATOR` в `IN_WORD` (т.е. когда после разделителя встречается первый символ слова), счетчик слов увеличивается на единицу; |
| 33 | +- при переходе из `IN_WORD` в `IN_SEPARATOR`, состояние просто меняется; |
| 34 | +- если состояние не меняется (например, два символа слова подряд), никаких действий со счетчиком не происходит. |
| 35 | +Этот подход позволяет обойтись без создания подстрок или сложных манипуляций с памятью, обеспечивая высокую производительность за счет одного линейного прохода по данным. |
| 36 | + |
| 37 | +## 4. Схема распараллеливания |
| 38 | + |
| 39 | +Параллельная версия реализована с использованием MPI по модели "Master/Worker". Процесс с рангом 0 выступает в роли "мастера", а все остальные процессы (включая ранг 0) — в роли "воркеров". |
| 40 | + |
| 41 | +1. **Распределение данных:** |
| 42 | + - процесс 0 определяет общую длину входной строки и рассылает это значение всем остальным процессам с помощью коллективной операции `MPI_Bcast`; |
| 43 | + - строка делится на N примерно равных непрерывных частей, где N — общее число процессов; |
| 44 | + - для распределения этих частей по всем процессам используется операция `MPI_Scatterv`, так как она позволяет работать с частями неодинакового размера (что актуально, если длина строки не делится нацело на число процессов). |
| 45 | + |
| 46 | +2. **Схема коммуникаций и обработка границ:** |
| 47 | + - главная проблема параллельного подсчета — слова, "разрезанные" на границе двух частей, принадлежащих разным процессам. Чтобы решить эту проблему, каждый процесс должен знать, каким символом закончилась часть его левого соседа; |
| 48 | + - для этого используется операция `MPI_Sendrecv`. Каждый процесс `p` отправляет свой последний символ процессу `p+1` и одновременно получает последний символ от процесса `p-1`. Это позволяет каждому процессу корректно определить, является ли первое слово в его части новым или продолжением слова из предыдущей части. `MPI_Sendrecv` гарантирует отсутствие взаимоблокировок при обмене. |
| 49 | + |
| 50 | +3. **Агрегация результатов:** |
| 51 | + - каждый процесс выполняет локальный подсчет слов в своей части данных с учетом полученного граничного символа; |
| 52 | + - локальные результаты со всех процессов собираются на процессе 0 и суммируются с помощью коллективной операции `MPI_Reduce` с оператором `MPI_SUM`. |
| 53 | + |
| 54 | + |
| 55 | +## 5. Детали реализации |
| 56 | + |
| 57 | +Реализация задачи выполнена на языке C++ и разделена на несколько логических компонентов в соответствии со структурой предоставленного фреймворка. |
| 58 | + |
| 59 | +### Структура кода |
| 60 | + |
| 61 | +- **`common/include/common.hpp`**: общий заголовочный файл, определяющий типы данных для задачи: |
| 62 | + - `InType = std::string`: входные данные — стандартная строка; |
| 63 | + - `OutType = int`: выходные данные — целое число (количество слов); |
| 64 | + - `BaseTask`: "псевдоним" для базового класса `ppc::task::Task`, |
| 65 | + |
| 66 | +- **`seq/`**: директория с последовательной реализацией: |
| 67 | + - `ops_seq.hpp`: объявление класса `VolkovACountWordLineSEQ`; |
| 68 | + - `ops_seq.cpp`: реализация методов класса и вспомогательных функций, |
| 69 | + |
| 70 | +- **`mpi/`**: директория с параллельной реализацией: |
| 71 | + - `ops_mpi.hpp`: объявление класса `VolkovACountWordLineMPI`; |
| 72 | + - `ops_mpi.cpp`: реализация методов класса и вспомогательных функций для MPI, |
| 73 | + |
| 74 | +- **`tests/`**: содержит функциональные и производительные тесты для проверки корректности и измерения производительности. |
| 75 | + |
| 76 | +### Ключевые классы и функции |
| 77 | + |
| 78 | +Решение инкапсулировано в классах `VolkovACountWordLineSEQ` и `VolkovACountWordLineMPI`, унаследованных от `BaseTask`. Основная вычислительная логика находится в переопределенном методе `RunImpl`. |
| 79 | + |
| 80 | +Для повышения читаемости, снижения когнитивной сложности основная логика подсчета слов была вынесена во внутренние вспомогательные функции, помещенные в анонимное пространство имен в соответствующих .cpp файлах: |
| 81 | + |
| 82 | +1. **`bool IsTokenChar(char c)`**: |
| 83 | + - простая функция-предикат, которая возвращает `true`, если переданный символ является частью слова (буква, цифра, `_` или `-`), и `false` в противном случае. Функция была вынесена для переиспользования и улучшения читаемости основного алгоритма, |
| 84 | + |
| 85 | +2. **`int CountWords(const char* data, size_t n)` (в `ops_seq.cpp`)**: |
| 86 | + - эта функция реализует основной алгоритм подсчета слов на основе "машины состояний" с двумя состояниями (`IN_WORD`, `IN_SEPARATOR`). Она проходит по данным за один проход, что обеспечивает высокую производительность, |
| 87 | + |
| 88 | +3. **`int CountWordsInChunk(const std::vector<char>& data, char prev_char)` (в `ops_mpi.cpp`)**: |
| 89 | + - адаптированная версия `CountWords` для MPI-реализации. Она принимает дополнительный аргумент `prev_char` — последний символ из блока данных предыдущего процесса. Этот символ используется для определения начального состояния машины состояний, что позволяет корректно обрабатывать слова, "разрезанные" на границах между процессами. |
| 90 | + |
| 91 | + |
| 92 | +## 6. Экспериментальные результаты |
| 93 | + |
| 94 | +### Окружение |
| 95 | + |
| 96 | +- **CPU:** Intel(R) Core(TM) i5-10400F CPU @ 2.90GHz (6 ядер, 12 потоков) |
| 97 | +- **ОС:** Ubuntu 22.04.2 LTS (запущенная через Docker Engive v28.5.2 на Windows 11) |
| 98 | +- **Компилятор:** g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 |
| 99 | + |
| 100 | +### Производительность |
| 101 | + |
| 102 | +Для тестов производительности использовался сгенерированный текст длиной 10 миллионов символов. Измерения времени проводились для последовательной реализации и для MPI-реализации на 2, 4 и 8 процессах. |
| 103 | + |
| 104 | +| Режим | Число процессов | Время, с | Ускорение (Speedup) | Эффективность (Efficiency) | |
| 105 | +|:------|:----------------:|:---------|:--------------------|:---------------------------| |
| 106 | +| seq | 1 | 0.0095 | 1.00 | 100.0% | |
| 107 | +| mpi | 2 | 0.0065 | 1.46 | 73.0% | |
| 108 | +| mpi | 4 | 0.0039 | 2.44 | 61.0% | |
| 109 | +| mpi | 8 | 0.0051 | 1.86 | 23.3% | |
| 110 | + |
| 111 | +**Выводы:** |
| 112 | +Полученные результаты демонстрируют несколько ключевых аспектов параллельного выполнения данной задачи на рассматриваемом окружении: |
| 113 | +1. *эффективное масштабирование до 4 процессов:* при увеличении числа процессов с 1 до 4 наблюдается значительное ускорение (до 2.44x). Это подтверждает, что выбранный алгоритм распараллеливания с использованием MPI эффективно распределяет работу, и накладные расходы не перевешивают выгоду от параллельных вычислений; |
| 114 | +2. *деградация производительности на 8 процессах:* время выполнения увеличилось с 0.0039 с до 0.0051 с, а эффективность упала до 23.3%. Это классический и довольно интересный пример ситуации, когда накладные расходы на параллелизм превысили выгоду. |
| 115 | + |
| 116 | +## 7. Выводы |
| 117 | + |
| 118 | +В ходе работы была успешно реализована и протестирована параллельная версия алгоритма подсчета слов в строке с использованием технологии MPI. Алгоритм продемонстрировал значительное ускорение по сравнению с последовательной версией, подтвердив эффективность выбранной схемы распараллеливания. Вот так :) |
| 119 | + |
| 120 | +## 8. Источники: |
| 121 | + |
| 122 | +1. лекции и практики курса "Параллельное программирование для кластерных систем"; |
| 123 | +2. стандарт MPI (форум MPI); |
| 124 | +3. документация по C++; |
0 commit comments