Skip to content

Commit f361bfa

Browse files
authored
Волков Алексей. Технология SEQ-MPI. Подсчет числа слов в строке. Вариант 24 (#118)
## Описание - **Задача**: Подсчет числа слов в строке - **Вариант**: 24 - **Технология**: SEQ. MPI - **Описание**: в рамках работы была выполнена последовательная (SEQ) и параллельная (MPI) реализация алгоритма подсчета слов. Последовательный алгоритм основан на эффективной однопроходной "машине состояний", которая отслеживает нахождение внутри слова или разделителя. Параллельный алгоритм MPI использует схему "Master/Worker", где процесс 0 распределяет данные равными частями с помощью MPI_Scatterv. --- ## Чек-лист - [x] **Статус CI**: Все CI-задачи (сборка, тесты, генерация отчёта) успешно проходят на моей ветке в моем форке - [x] **Директория и именование задачи**: Я создал директорию с именем `<фамилия>_<первая_буква_имени>_<короткое_название_задачи>` - [x] **Полное описание задачи**: Я предоставил полное описание задачи в теле pull request - [x] **clang-format**: Мои изменения успешно проходят `clang-format` локально в моем форке (нет ошибок форматирования) - [x] **clang-tidy**: Мои изменения успешно проходят `clang-tidy` локально в моем форке (нет предупреждений/ошибок) - [x] **Функциональные тесты**: Все функциональные тесты успешно проходят локально на моей машине - [x] **Тесты производительности**: Все тесты производительности успешно проходят локально на моей машине - [x] **Ветка**: Я работаю в ветке, названной точно так же, как директория моей задачи (например, `nesterov_a_vector_sum`), а не в `master` - [x] **Правдивое содержание**: Я подтверждаю, что все сведения, указанные в этом pull request, являются точными и достоверными
1 parent d6c78ef commit f361bfa

File tree

12 files changed

+29494
-0
lines changed

12 files changed

+29494
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <tuple>
5+
6+
#include "task/include/task.hpp"
7+
8+
namespace volkov_a_count_word_line {
9+
10+
using InType = std::string;
11+
using OutType = int;
12+
using TestType = std::tuple<InType, OutType>;
13+
using BaseTask = ppc::task::Task<InType, OutType>;
14+
15+
} // namespace volkov_a_count_word_line

tasks/volkov_a_count_word_line/data/Tolstoy Leo. War and Peace.txt

Lines changed: 28913 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"student": {
3+
"first_name": "Алексей",
4+
"last_name": "Волков",
5+
"middle_name": "Иванович",
6+
"group_number": "3823Б1ФИ2",
7+
"task_number": "1"
8+
}
9+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "task/include/task.hpp"
4+
#include "volkov_a_count_word_line/common/include/common.hpp"
5+
6+
namespace volkov_a_count_word_line {
7+
8+
class VolkovACountWordLineMPI : public BaseTask {
9+
public:
10+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
11+
return ppc::task::TypeOfTask::kMPI;
12+
}
13+
explicit VolkovACountWordLineMPI(const InType &in);
14+
15+
private:
16+
bool ValidationImpl() override;
17+
bool PreProcessingImpl() override;
18+
bool RunImpl() override;
19+
bool PostProcessingImpl() override;
20+
};
21+
22+
} // namespace volkov_a_count_word_line
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#include "volkov_a_count_word_line/mpi/include/ops_mpi.hpp"
2+
3+
#include <mpi.h>
4+
5+
#include <cstddef>
6+
#include <string>
7+
#include <vector>
8+
9+
#include "volkov_a_count_word_line/common/include/common.hpp"
10+
11+
namespace volkov_a_count_word_line {
12+
13+
namespace {
14+
15+
bool IsTokenChar(char c) {
16+
const bool is_alpha = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
17+
const bool is_digit = (c >= '0' && c <= '9');
18+
const bool is_special = (c == '-' || c == '_');
19+
return is_alpha || is_digit || is_special;
20+
}
21+
22+
int CountWordsInChunk(const std::vector<char> &data, int valid_size) {
23+
int count = 0;
24+
bool in_word = false;
25+
26+
for (int i = 0; i < valid_size; ++i) {
27+
if (IsTokenChar(data[i])) {
28+
if (!in_word) {
29+
in_word = true;
30+
count++;
31+
}
32+
} else {
33+
in_word = false;
34+
}
35+
}
36+
37+
if (in_word && static_cast<size_t>(valid_size) < data.size() && IsTokenChar(data[valid_size])) {
38+
count--;
39+
}
40+
41+
return count;
42+
}
43+
44+
int CountWordsSeq(const std::string &str) {
45+
int count = 0;
46+
bool in_word = false;
47+
for (char c : str) {
48+
if (IsTokenChar(c)) {
49+
if (!in_word) {
50+
in_word = true;
51+
count++;
52+
}
53+
} else {
54+
in_word = false;
55+
}
56+
}
57+
return count;
58+
}
59+
60+
} // namespace
61+
62+
VolkovACountWordLineMPI::VolkovACountWordLineMPI(const InType &in) {
63+
SetTypeOfTask(GetStaticTypeOfTask());
64+
GetInput() = in;
65+
GetOutput() = 0;
66+
}
67+
68+
bool VolkovACountWordLineMPI::ValidationImpl() {
69+
return true;
70+
}
71+
72+
bool VolkovACountWordLineMPI::PreProcessingImpl() {
73+
return true;
74+
}
75+
76+
bool VolkovACountWordLineMPI::RunImpl() {
77+
int rank = 0;
78+
int world_size = 0;
79+
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
80+
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
81+
82+
std::string &input_str = GetInput();
83+
84+
if (input_str.empty()) {
85+
GetOutput() = 0;
86+
return true;
87+
}
88+
89+
if (static_cast<size_t>(world_size) > input_str.size()) {
90+
if (rank == 0) {
91+
GetOutput() = CountWordsSeq(input_str);
92+
}
93+
MPI_Bcast(&GetOutput(), 1, MPI_INT, 0, MPI_COMM_WORLD);
94+
return true;
95+
}
96+
97+
size_t remainder = input_str.size() % static_cast<size_t>(world_size);
98+
size_t padding = (static_cast<size_t>(world_size) - remainder) % static_cast<size_t>(world_size);
99+
input_str.append(padding + static_cast<size_t>(world_size), ' ');
100+
101+
int chunk_size = static_cast<int>(input_str.size() / static_cast<size_t>(world_size));
102+
103+
std::vector<int> send_counts(world_size);
104+
std::vector<int> displs(world_size);
105+
106+
if (rank == 0) {
107+
for (int i = 0; i < world_size; ++i) {
108+
send_counts[i] = chunk_size + 1;
109+
displs[i] = i * chunk_size;
110+
}
111+
}
112+
113+
std::vector<char> local_data(chunk_size + 1);
114+
115+
MPI_Scatterv(input_str.data(), send_counts.data(), displs.data(), MPI_CHAR, local_data.data(), chunk_size + 1,
116+
MPI_CHAR, 0, MPI_COMM_WORLD);
117+
118+
int local_words = CountWordsInChunk(local_data, chunk_size);
119+
int total_words = 0;
120+
121+
MPI_Allreduce(&local_words, &total_words, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
122+
123+
GetOutput() = total_words;
124+
125+
return true;
126+
}
127+
128+
bool VolkovACountWordLineMPI::PostProcessingImpl() {
129+
return true;
130+
}
131+
132+
} // namespace volkov_a_count_word_line
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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++;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include "task/include/task.hpp"
4+
#include "volkov_a_count_word_line/common/include/common.hpp"
5+
6+
namespace volkov_a_count_word_line {
7+
8+
class VolkovACountWordLineSEQ : public BaseTask {
9+
public:
10+
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
11+
return ppc::task::TypeOfTask::kSEQ;
12+
}
13+
explicit VolkovACountWordLineSEQ(const InType &in);
14+
15+
private:
16+
bool ValidationImpl() override;
17+
bool PreProcessingImpl() override;
18+
bool RunImpl() override;
19+
bool PostProcessingImpl() override;
20+
};
21+
22+
} // namespace volkov_a_count_word_line

0 commit comments

Comments
 (0)