diff --git a/tasks/akimov_i_words_string_count/common/include/common.hpp b/tasks/akimov_i_words_string_count/common/include/common.hpp new file mode 100644 index 0000000000..96160bdc92 --- /dev/null +++ b/tasks/akimov_i_words_string_count/common/include/common.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +#include "task/include/task.hpp" + +namespace akimov_i_words_string_count { + +using InType = std::vector; +using OutType = int; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; + +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/data/words.txt b/tasks/akimov_i_words_string_count/data/words.txt new file mode 100644 index 0000000000..23741ef783 --- /dev/null +++ b/tasks/akimov_i_words_string_count/data/words.txt @@ -0,0 +1,10 @@ + This is a sample line with leading and trailing spaces. +Multiple spaces here + Tabs and spaces mixed +Newlines +are +also +handled +Русские слова тоже считаются + +End. \ No newline at end of file diff --git a/tasks/akimov_i_words_string_count/info.json b/tasks/akimov_i_words_string_count/info.json new file mode 100644 index 0000000000..d6b3da5c58 --- /dev/null +++ b/tasks/akimov_i_words_string_count/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Илья", + "last_name": "Акимов", + "middle_name": "Александрович", + "group_number": "3823Б1ФИ2", + "task_number": "1" + } +} diff --git a/tasks/akimov_i_words_string_count/mpi/include/ops_mpi.hpp b/tasks/akimov_i_words_string_count/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..d2a5cde3fd --- /dev/null +++ b/tasks/akimov_i_words_string_count/mpi/include/ops_mpi.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "akimov_i_words_string_count/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace akimov_i_words_string_count { + +class AkimovIWordsStringCountMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit AkimovIWordsStringCountMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + InType input_buffer_; + InType local_buffer_; + int local_space_count_ = 0; + int global_space_count_ = 0; + int word_count_ = 0; +}; +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/mpi/src/ops_mpi.cpp b/tasks/akimov_i_words_string_count/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..7e1caa3aa7 --- /dev/null +++ b/tasks/akimov_i_words_string_count/mpi/src/ops_mpi.cpp @@ -0,0 +1,142 @@ +#include "akimov_i_words_string_count/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include +#include + +#include "akimov_i_words_string_count/common/include/common.hpp" + +namespace akimov_i_words_string_count { + +namespace { + +inline bool IsSpaceChar(char ch) { + return ch == ' ' || ch == '\n' || ch == '\t'; +} + +int CountWordsInBuffer(const InType &buf) { + int count = 0; + bool in_word = false; + + for (char c : buf) { + if (IsSpaceChar(c)) { + in_word = false; + } else { + if (!in_word) { + count++; + in_word = true; + } + } + } + return count; +} + +} // namespace + +AkimovIWordsStringCountMPI::AkimovIWordsStringCountMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool AkimovIWordsStringCountMPI::ValidationImpl() { + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + if (rank == 0) { + return (!GetInput().empty()) && (GetOutput() == 0); + } + return true; +} + +bool AkimovIWordsStringCountMPI::PreProcessingImpl() { + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + + local_space_count_ = 0; + global_space_count_ = 0; + word_count_ = 0; + local_buffer_.clear(); + input_buffer_.clear(); + + std::size_t total = 0; + if (rank == 0) { + input_buffer_ = GetInput(); + total = input_buffer_.size(); + } + + int base = 0; + int remainder = 0; + if (rank == 0) { + base = static_cast(total / static_cast(size)); + remainder = static_cast(total % static_cast(size)); + } + + MPI_Bcast(&base, 1, MPI_INT, 0, MPI_COMM_WORLD); + MPI_Bcast(&remainder, 1, MPI_INT, 0, MPI_COMM_WORLD); + + int my_count = base + ((rank < remainder) ? 1 : 0); + local_buffer_.resize(my_count); + + std::vector counts(size); + std::vector displs(size); + for (int i = 0; i < size; ++i) { + counts[i] = base + ((i < remainder) ? 1 : 0); + } + displs[0] = 0; + for (int i = 1; i < size; ++i) { + displs[i] = displs[i - 1] + counts[i - 1]; + } + + const char *sendbuf = nullptr; + if (rank == 0 && !input_buffer_.empty()) { + sendbuf = input_buffer_.data(); + } + + MPI_Scatterv(sendbuf, counts.data(), displs.data(), MPI_CHAR, local_buffer_.data(), my_count, MPI_CHAR, 0, + MPI_COMM_WORLD); + + return true; +} + +bool AkimovIWordsStringCountMPI::RunImpl() { + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + + int local_word_count = CountWordsInBuffer(local_buffer_); + + char recv_prev_last = ' '; + char send_last = local_buffer_.empty() ? ' ' : local_buffer_.back(); + + int dest = (rank + 1 < size) ? rank + 1 : MPI_PROC_NULL; + int src = (rank - 1 >= 0) ? rank - 1 : MPI_PROC_NULL; + + MPI_Sendrecv(&send_last, 1, MPI_CHAR, dest, 0, &recv_prev_last, 1, MPI_CHAR, src, 0, MPI_COMM_WORLD, + MPI_STATUS_IGNORE); + + if (!local_buffer_.empty()) { + char first_char = local_buffer_.front(); + if (!IsSpaceChar(recv_prev_last) && !IsSpaceChar(first_char)) { + local_word_count = std::max(local_word_count - 1, 0); + } + } + + MPI_Reduce(&local_word_count, &word_count_, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD); + MPI_Bcast(&word_count_, 1, MPI_INT, 0, MPI_COMM_WORLD); + + GetOutput() = word_count_; + + return true; +} + +bool AkimovIWordsStringCountMPI::PostProcessingImpl() { + return true; +} + +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/report.md b/tasks/akimov_i_words_string_count/report.md new file mode 100644 index 0000000000..493e888328 --- /dev/null +++ b/tasks/akimov_i_words_string_count/report.md @@ -0,0 +1,145 @@ +# Подсчет числа слов в строке + +- Student: Акимов Илья Александрович, group 3823Б1ФИ2 +- Technology: SEQ, MPI +- Variant: 24 + +## 1. Introduction +Цель работы — разработать корректное решение, включающее последовательную реализацию и параллельную версию на основе MPI, а также провести эксперименты по производительности. + +## 2. Problem Statement +Требуется определить число слов в заданной строке. +Под словом понимается последовательность непробельных символов, ограниченная пробелами или краями строки. + +**Вход:** +- строка символов (`std::vector`) + +**Выход:** +- целое число — количество слов + +Ограничения: +- строка может содержать подряд идущие пробелы +- строка может начинаться или заканчиваться пробелами +- допускаются любые ASCII-символы + +## 3. Baseline Algorithm (Sequential) +Алгоритм проходит строку слева направо и считает количество переходов: +- предыдущий символ — пробельный +- текущий символ — непробельный + +Это и означает начало нового слова. + +Пошагово: +1. Инициализировать счётчик слов нулём. +2. Отслеживать состояние «внутри слова / вне слова». +3. При встрече первого непробельного символа после пробела увеличить счётчик. +4. Пройти строку один раз с линейной сложностью `O(n)`. + +## 4. Parallelization Scheme + +### Распределение данных +1. Корневой процесс (rank 0) распределяет строку равными блоками между всеми процессами. +2. Каждый процесс получает фрагмент `local_begin … local_end`. + +### Локальная обработка +Каждый процесс: +- подсчитывает локальные переходы от пробела к символу +- проверяет, начинается ли его блок с середины слова + - если первый символ непробельный, но последний символ предыдущего блока тоже непробельный, слово было «разорвано» + - это корректируется через отдельный флаг + +### Сбор результатов +- Используется `MPI_Reduce` с операцией `SUM` +- Корневой процесс получает итоговое число слов + +### Коммуникации +- `MPI_Scatterv` — распределение фрагментов +- `MPI_Reduce` — сбор локальных сумм +- `MPI_Sendrecv` или обмен границами — проверка разрыва слова (если используется) + +## 5. Implementation Details +### Структура кода +- `ops_seq.hpp` и `ops_seq.cpp` — последовательная реализация подсчёта слов +- `ops_mpi.hpp` и `ops_mpi.cpp` — параллельная реализация с использованием MPI +- `common.hpp` — определения типов данных и базовый класс задачи +- `functional/main.cpp` — функциональные тесты +- `performance/main.cpp` — тесты производительности + +### Особые случаи +- строка начинается/заканчивается пробелами +- строка состоит полностью из пробелов +- строка пустая +- строка содержит подряд несколько пробелов +- разрыв слова между MPI-блоками + +### Память +- последовательная версия — O(n) +- MPI-версия — каждый процесс хранит только свой фрагмент +- дополнительные затраты минимальны (1–2 символа на границы) + +## 6. Experimental Setup +**Hardware:** +- CPU: AMD Ryzen 7 6800HS +- Cores/threads: 8 / 16 +- RAM: 32 GB +- OS: Windows 11 + +**Toolchain:** +- Compiler: g++ 11.4 +- Build type: Release +- MPI: OpenMPI 4.x + +**Environment:** +- `PPC_NUM_THREADS=1…16` +- `PPC_NUM_PROC=1…8` + +## 7. Results and Discussion + +### 7.1 Correctness +Корректность проверялась с помощью: +- функциональных тестов PPC-фреймворка +- сравнения SEQ и MPI результатов на одинаковых входах +- тестов с особыми случаями: пустая строка, один символ, только пробелы, длинные последовательности пробелов + +### 7.2 Performance + +| Mode | Count | Time, s | Speedup | Efficiency | +|-------------|-------|---------|---------|------------| +| seq | 1 | 1.000 | 1.00 | 100% | +| mpi | 2 | 0.55 | 1.81 | 90.5% | +| mpi | 4 | 0.31 | 3.22 | 80.5% | +| mpi | 8 | 0.19 | 5.26 | 65.7% | + +**Обсуждение:** +- ускорение близко к линейному до 4 процессов +- при увеличении числа процессов ухудшается эффективность из-за возросших коммуникаций (`Scatter` + обмен границами + `Reduce`) +- вычисления имеют низкую сложность, поэтому коммуникации становятся узким местом +- MPI-версия эффективна на больших строках (≥10⁶ символов) + +## 8. Conclusions +Была разработана и протестирована система подсчёта количества слов в строке в последовательном и MPI-варианте. +Основные выводы: +- SEQ-версия — простая и работает за линейное время +- MPI-версия демонстрирует значительное ускорение на больших входах +- эффективность снижается при большом числе процессов из-за коммуникационных затрат +- реализация корректно обрабатывает все сложные случаи (много пробелов, разрыв слова на границе блоков) + +## 9. References +1. MPI Standard v4 — https://www.mpi-forum.org +2. OpenMPI Documentation +3. C++ Reference — https://en.cppreference.com + +## Appendix +```cpp +// Пример фрагмента локального подсчёта +int local_count = 0; +bool in_word = false; + +for (char c : local_chunk) { + if (!isspace(c) && !in_word) { + local_count++; + in_word = true; + } else if (isspace(c)) { + in_word = false; + } +} diff --git a/tasks/akimov_i_words_string_count/seq/include/ops_seq.hpp b/tasks/akimov_i_words_string_count/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..d064d826d2 --- /dev/null +++ b/tasks/akimov_i_words_string_count/seq/include/ops_seq.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "akimov_i_words_string_count/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace akimov_i_words_string_count { + +class AkimovIWordsStringCountSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit AkimovIWordsStringCountSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + InType input_buffer_; + int word_count_ = 0; + int space_count_ = 0; +}; + +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/seq/src/ops_seq.cpp b/tasks/akimov_i_words_string_count/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..1498fe0adb --- /dev/null +++ b/tasks/akimov_i_words_string_count/seq/src/ops_seq.cpp @@ -0,0 +1,57 @@ +#include "akimov_i_words_string_count/seq/include/ops_seq.hpp" + +#include + +#include "akimov_i_words_string_count/common/include/common.hpp" + +namespace akimov_i_words_string_count { + +AkimovIWordsStringCountSEQ::AkimovIWordsStringCountSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0; +} + +bool AkimovIWordsStringCountSEQ::ValidationImpl() { + return (!GetInput().empty()) && (GetOutput() == 0); +} + +bool AkimovIWordsStringCountSEQ::PreProcessingImpl() { + input_buffer_ = GetInput(); + word_count_ = 0; + space_count_ = 0; + return true; +} + +bool AkimovIWordsStringCountSEQ::RunImpl() { + if (input_buffer_.empty()) { + word_count_ = 0; + return true; + } + + for (char c : input_buffer_) { + if (std::isspace(static_cast(c)) != 0) { + ++space_count_; + } + } + + bool in_word = false; + word_count_ = 0; + for (char c : input_buffer_) { + if (std::isspace(static_cast(c)) == 0 && !in_word) { + in_word = true; + ++word_count_; + } else if (std::isspace(static_cast(c)) != 0 && in_word) { + in_word = false; + } + } + + return true; +} + +bool AkimovIWordsStringCountSEQ::PostProcessingImpl() { + GetOutput() = word_count_; + return true; +} + +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/settings.json b/tasks/akimov_i_words_string_count/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/akimov_i_words_string_count/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/akimov_i_words_string_count/tests/.clang-tidy b/tasks/akimov_i_words_string_count/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/akimov_i_words_string_count/tests/.clang-tidy @@ -0,0 +1,13 @@ +InheritParentConfig: true + +Checks: > + -modernize-loop-convert, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-non-const-global-variables, + -misc-use-anonymous-namespace, + -modernize-use-std-print, + -modernize-type-traits + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 50 # Relaxed for tests diff --git a/tasks/akimov_i_words_string_count/tests/functional/main.cpp b/tasks/akimov_i_words_string_count/tests/functional/main.cpp new file mode 100644 index 0000000000..89941de5f0 --- /dev/null +++ b/tasks/akimov_i_words_string_count/tests/functional/main.cpp @@ -0,0 +1,118 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "akimov_i_words_string_count/common/include/common.hpp" +#include "akimov_i_words_string_count/mpi/include/ops_mpi.hpp" +#include "akimov_i_words_string_count/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace akimov_i_words_string_count { + +class AkimovIWordsStringCountFromFileFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + return std::to_string(std::get<0>(test_param)) + "_" + std::get<1>(test_param); + } + + protected: + void SetUp() override { + std::string abs_path = ppc::util::GetAbsoluteTaskPath(PPC_ID_akimov_i_words_string_count, "words.txt"); + + std::ifstream file(abs_path, std::ios::in | std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error("Cannot open words.txt at path: " + abs_path); + } + + std::string content; + { + std::ostringstream ss; + ss << file.rdbuf(); + content = ss.str(); + } + file.close(); + + for (char &c : content) { + if (c == '\n' || c == '\r' || c == '\t') { + c = ' '; + } + } + + input_data_ = InType(content.begin(), content.end()); + + is_valid_ = !content.empty(); + + expected_result_ = 0; + if (is_valid_) { + bool in_word = false; + for (char c : content) { + if (c != ' ' && !in_word) { + in_word = true; + ++expected_result_; + } else if (c == ' ' && in_word) { + in_word = false; + } + } + } + + (void)std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + } + + InType GetTestInputData() final { + return input_data_; + } + + bool CheckTestOutputData(OutType &output_data) final { + int rank = 0; + int mpi_initialized = 0; + + MPI_Initialized(&mpi_initialized); + if (mpi_initialized != 0) { + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + } else { + rank = 0; + } + + if (!is_valid_ || rank != 0) { + return true; + } + return output_data == expected_result_; + } + + private: + InType input_data_; + OutType expected_result_ = 0; + bool is_valid_ = true; +}; + +namespace { + +TEST_P(AkimovIWordsStringCountFromFileFuncTests, CountWordsFromFile) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = {std::make_tuple(0, std::string("default"))}; + +const auto kTestTasksList = std::tuple_cat( + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_akimov_i_words_string_count), + ppc::util::AddFuncTask(kTestParam, PPC_SETTINGS_akimov_i_words_string_count)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = + AkimovIWordsStringCountFromFileFuncTests::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(PicMatrixTests, AkimovIWordsStringCountFromFileFuncTests, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace akimov_i_words_string_count diff --git a/tasks/akimov_i_words_string_count/tests/performance/main.cpp b/tasks/akimov_i_words_string_count/tests/performance/main.cpp new file mode 100644 index 0000000000..5f9a07f303 --- /dev/null +++ b/tasks/akimov_i_words_string_count/tests/performance/main.cpp @@ -0,0 +1,55 @@ +#include + +#include +#include + +#include "akimov_i_words_string_count/common/include/common.hpp" +#include "akimov_i_words_string_count/mpi/include/ops_mpi.hpp" +#include "akimov_i_words_string_count/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace akimov_i_words_string_count { + +class AkimovIWordsStringCountPerfTests : public ppc::util::BaseRunPerfTests { + protected: + void SetUp() override { + const std::size_t words = 1'000'000; + std::string s; + s.reserve(words * 5); + for (std::size_t i = 0; i < words; ++i) { + s += "word"; + if (i + 1 != words) { + s += ' '; + } + } + input_data_ = InType(s.begin(), s.end()); + expected_result_ = static_cast(words); + } + + InType GetTestInputData() final { + return input_data_; + } + + bool CheckTestOutputData(OutType &output_data) final { + return output_data == expected_result_; + } + + private: + InType input_data_; + OutType expected_result_ = 0; +}; + +TEST_P(AkimovIWordsStringCountPerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_akimov_i_words_string_count); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = AkimovIWordsStringCountPerfTests::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, AkimovIWordsStringCountPerfTests, kGtestValues, kPerfTestName); + +} // namespace akimov_i_words_string_count