Skip to content
16 changes: 16 additions & 0 deletions tasks/gasenin_l_lex_dif/common/include/common.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once

#include <string>
#include <tuple>
#include <utility>

#include "task/include/task.hpp"

namespace gasenin_l_lex_dif {

using InType = std::pair<std::string, std::string>;
using OutType = int;
using TestType = std::tuple<int, std::string>;
using BaseTask = ppc::task::Task<InType, OutType>;

} // namespace gasenin_l_lex_dif
9 changes: 9 additions & 0 deletions tasks/gasenin_l_lex_dif/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"student": {
"first_name": "Леонид",
"last_name": "Гасенин",
"middle_name": "Вячеславович",
"group_number": "3823Б1ФИ3",
"task_number": "1"
}
}
30 changes: 30 additions & 0 deletions tasks/gasenin_l_lex_dif/mpi/include/ops_mpi.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include "gasenin_l_lex_dif/common/include/common.hpp"
#include "task/include/task.hpp"

namespace gasenin_l_lex_dif {

/*struct DiffInfo {
size_t pos;
int result;
};*/

class GaseninLLexDifMPI : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kMPI;
}
explicit GaseninLLexDifMPI(const InType &in);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;

// void RunSequentialComparison(const std::string &str1, const std::string &str2);
// bool RunParallelComparison(const std::string &str1, const std::string &str2, int rank, int size);
};

} // namespace gasenin_l_lex_dif
103 changes: 103 additions & 0 deletions tasks/gasenin_l_lex_dif/mpi/src/ops_mpi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#include "gasenin_l_lex_dif/mpi/include/ops_mpi.hpp"

#include <mpi.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string> // Исправление 1: Явное подключение string

#include "gasenin_l_lex_dif/common/include/common.hpp"

namespace {

// Выносим логику локального поиска различий, чтобы снизить Cognitive Complexity
struct LocalDiff {
size_t diff_pos;
int result;
};

LocalDiff FindLocalDifference(const std::string &str1, const std::string &str2, size_t start, size_t end) {
for (size_t i = start; i < end; ++i) {
char c1 = (i < str1.length()) ? str1[i] : '\0';
char c2 = (i < str2.length()) ? str2[i] : '\0';

if (c1 != c2) {
// Исправление 2: Используем designated initializers (.field = value)
return {.diff_pos = i, .result = (c1 < c2) ? -1 : 1};
}
}
// Исправление 3: Используем designated initializers
return {.diff_pos = std::string::npos, .result = 0};
}

} // namespace

namespace gasenin_l_lex_dif {

GaseninLLexDifMPI::GaseninLLexDifMPI(const InType &in) {
SetTypeOfTask(GetStaticTypeOfTask());
GetInput() = in;
GetOutput() = 0;
}

bool GaseninLLexDifMPI::ValidationImpl() {
const auto &[str1, str2] = GetInput();
return str1.length() <= 10000000 && str2.length() <= 10000000;
}

bool GaseninLLexDifMPI::PreProcessingImpl() {
return true;
}

bool GaseninLLexDifMPI::RunImpl() {
const auto &[str1, str2] = GetInput();
int rank = 0;
int size = 0;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);

size_t total_len = std::max(str1.length(), str2.length());

if (total_len == 0) {
GetOutput() = 0;
return true;
}

size_t chunk_size = (total_len + size - 1) / size;
size_t start = rank * chunk_size;
size_t end = std::min(start + chunk_size, total_len);

// Используем вспомогательную функцию
LocalDiff local_diff = FindLocalDifference(str1, str2, start, end);
size_t local_diff_pos = (local_diff.diff_pos == std::string::npos) ? total_len : local_diff.diff_pos;
int local_result = local_diff.result;

auto local_pos_64 = static_cast<uint64_t>(local_diff_pos);
uint64_t global_min_pos_64 = 0;

MPI_Allreduce(&local_pos_64, &global_min_pos_64, 1, MPI_UINT64_T, MPI_MIN, MPI_COMM_WORLD);

auto global_min_pos = static_cast<size_t>(global_min_pos_64);
int result_for_sum = (local_diff_pos == global_min_pos) ? local_result : 0;

int final_result = 0;
MPI_Allreduce(&result_for_sum, &final_result, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);

if (global_min_pos == total_len) {
if (str1.length() != str2.length()) {
final_result = (str1.length() < str2.length()) ? -1 : 1;
} else {
final_result = 0;
}
}

GetOutput() = final_result;
return true;
}

bool GaseninLLexDifMPI::PostProcessingImpl() {
return true;
}

} // namespace gasenin_l_lex_dif
117 changes: 117 additions & 0 deletions tasks/gasenin_l_lex_dif/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Проверка лексикографической упорядоченности двух строк

- Студент: Гасенин Леонид Вячеславович, группа 3823Б1ФИ3
- Технология: MPI, SEQ
- Вариант: 26

## 1. Введение
Лексикографическое сравнение строк является фундаментальной операцией в информатике, используемой для сортировки, поиска и обработки данных. Данный проект реализует параллельные и последовательные алгоритмы для сравнения двух строк лексикографически, определяя их относительный порядок согласно кодам символов. Цель работы - достижение улучшения производительности через параллельную обработку при сохранении корректности результатов.

## 2. Постановка задачи
Даны две строки A и B, необходимо определить их лексикографическое отношение:
- Вернуть -1 если A < B
- Вернуть 0 если A = B
- Вернуть 1 если A > B

Входные данные представляют собой пару строк (`std::pair<std::string, std::string>`), выходные данные - целочисленный результат сравнения (`int`). Ограничением является максимальная длина строки в 10,000,000 символов.

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

Последовательный алгоритм (реализован в `ops_seq.cpp`) является эталонной реализацией стандартного лексикографического сравнения.

1. Определяется минимальная длина из двух строк (`min_len`).
2. Запускается цикл, итерирующийся посимвольно от `i = 0` до `min_len`.
3. На каждой итерации сравниваются символы `str1[i]` и `str2[i]`.
4. При первом же расхождении (`str1[i] != str2[i]`) функция немедленно возвращает результат: -1, если `str1[i] < str2[i]`, или 1 в противном случае.
5. Если цикл завершается без нахождения различий, это означает, что строки либо идентичны, либо одна является префиксом другой.
6. В этом случае сравнивается их общая длина:
* Если `str1.length() < str2.length()`, возвращается -1.
* Если `str1.length() > str2.length()`, возвращается 1.
* Если длины равны, возвращается 0.

## 4. Схема распараллеливания
Схема распараллеливания (реализованная в `ops_mpi.cpp`) основана на декомпозиции данных (Data Parallelism).

1. **Распределение данных:** Максимальная длина из двух строк (`total_len = std::max(str1.length(), str2.length())`) делится на `N` равных (или почти равных) блоков, где `N` — число MPI-процессов. Каждый процесс (`rank`) получает для анализа свой диапазон индексов (от `start` до `end`).
2. **Локальный поиск:** Каждый процесс итерируется по своему диапазону и ищет *первое* расхождение символов. Если оно найдено, процесс сохраняет позицию `local_diff_pos` и результат `local_result` (-1 или 1). Если в блоке расхождений нет, `local_diff_pos` остается равным "бесконечности" (`total_len`).
3. **Коллективные операции (Редукция):** Для нахождения *глобального* результата используется двухэтапная редукция:
* **Этап 1 (Поиск min. позиции):** Выполняется `MPI_Allreduce` с операцией `MPI_MIN` по всем `local_diff_pos`. В результате все процессы узнают `global_min_pos` — самую первую (минимальную) позицию, на которой было найдено расхождение во *всех* блоках.
* **Этап 2 (Сбор результата):** Выполняется `MPI_Allreduce` с операцией `MPI_SUM`. Только тот процесс, у которого `local_diff_pos == global_min_pos`, передает в эту операцию свой `local_result`. Остальные процессы передают 0. Итоговая сумма и будет являться финальным результатом (-1, 1 или 0, если никто не нашел разницы).
4. **Обработка префиксов:** Если `global_min_pos` остался равен `total_len` (расхождений не найдено), выполняется проверка длин строк, аналогичная последовательному алгоритму.


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

- **Структура кода:**
- `common.hpp`: Определяет общие типы `InType` (`std::pair<std::string, std::string>`), `OutType` (`int`) и базовый класс `BaseTask`.
- `ops_seq.hpp` / `ops_seq.cpp`: Реализация последовательного алгоритма (`GaseninLLexDifSEQ`).
- `ops_mpi.hpp` / `ops_mpi.cpp`: Реализация параллельного MPI-алгоритма (`GaseninLLexDifMPI`).
- `main.cpp`: GTest-тесты для проверки корректности (`GaseninLRunFuncTestsLexDif`).
- **Важные моменты:**
- **Сравнение разной длины:** При параллельном поиске, если индекс `i` выходит за пределы одной из строк, для сравнения используется `\0`, что позволяет корректно обрабатывать префиксы (`char c1 = (i < str1.length()) ? str1[i] : '\0';`).
- **Обработка пустых строк:** Случай, когда обе строки пусты (`total_len == 0`), обрабатывается отдельно в начале `RunImpl` и немедленно возвращает 0.
- **Коммуникации:** Вместо сбора всех локальных результатов на один процесс (`MPI_Gather`) и последующей рассылки (`MPI_Bcast`), используется две эффективные операции `MPI_Allreduce`, которые сразу доставляют финальный результат всем процессам.

## 6. Экспериментальная установка

- **Hardware/OS:**
- CPU: Intel Core i5-8400 2.80ghz
- RAM: 8 ГБ
- OS: Windows 10
- **Toolchain:**
- IDE: Visual Studio Code
- Компилятор: GCC
- Система сборки: CMake
- Система контроля версий: Git
- **Environment:**
- `PPC_NUM_PROC`: Варьировалось (1, 2, 4, ...) для MPI-тестов.
- **Data:**
- **Функциональные тесты** (`main.cpp`): используют предопределенные пары строк для покрытия различных случаев (см. 7.1).
- **Тесты производительности:** используют две строки по 10,000,000 символов, с одним различием в середине (`long_str2[5000000] = 'b';`).

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

### 7.1 Корректность
Верификация корректности проводилась комплексно через набор модульных тестов GTest (определенных в `main.cpp`). Тестовые случаи включали:

- `apple_banana` (A < B)
- `hello_hello` (A == B)
- `zebra_apple` (A > B)
- `empty_first` ("" < "test")
- `empty_second` ("test" > "")
- `both_empty` ("" == "")
- `different_length` ("abc" < "abcd")
- `same_prefix` ("abcdef" < "abcxyz")
- `unicode_test` ("привет" > "пока")
- `case_sensitive` ("Apple" < "apple")

Результаты параллельной MPI-реализации полностью совпали с результатами эталонной последовательной версии на всех тестах.

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

Для оценки производительности использовались тесты на длинных строках (10 млн символов).

| Режим | Процессы | Время, с | Ускорение | Эффективность |
|-------|----------|----------|-----------|---------------|
| seq | 1 | 0.092 | 1.00 | N/A |
| mpi | 2 | 0.051 | 1.80 | 90.0% |
| mpi | 4 | 0.031 | 2.97 | 74.2% |

**Обсуждение:**
- **Масштабируемость:** Алгоритм демонстрирует хорошую масштабируемость для задач с длинными строками. Ускорение близко к 3x на 4 процессах.
- **Накладные расходы:** Эффективность снижается при добавлении процессов (с 90% до 74.2%) из-за коммуникационных издержек. Две операции `MPI_Allreduce` являются барьерами синхронизации, и их относительная стоимость растет.
- **Ограничения:** Для *коротких* строк (например, < 10000 символов) накладные расходы на запуск MPI и выполнение двух операций `Allreduce` почти наверняка превысят выгоду от параллельных вычислений, делая MPI-версию медленнее последовательной.

## 8. Выводы

Реализация успешно демонстрирует возможности параллелизации задачи лексикографического сравнения строк. Достигнуто ускорение до 3 раз при сохранении полной корректности результатов. Алгоритм эффективно обрабатывает различные сценарии сравнения и граничные случаи.

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

Практическая ценность работы заключается в демонстрации подхода к параллелизации задач с зависимостями по данным и необходимости координации между процессами для обеспечения корректности.

## 9. Ссылки
1. OpenMPI документация: https://www.open-mpi.org/
2. MPI Standard: https://www.mpi-forum.org/docs/
3. Introduction to Parallel Computing: https://computing.llnl.gov/tutorials/parallel_comp/
4. Документация по курсу https://learning-process.github.io/parallel_programming_course/ru/index.html
28 changes: 28 additions & 0 deletions tasks/gasenin_l_lex_dif/seq/include/ops_seq.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#pragma once

#include <string>

#include "gasenin_l_lex_dif/common/include/common.hpp"
#include "task/include/task.hpp"

namespace gasenin_l_lex_dif {

class GaseninLLexDifSEQ : public BaseTask {
public:
static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() {
return ppc::task::TypeOfTask::kSEQ;
}
explicit GaseninLLexDifSEQ(const InType &in);

static InType ReadInteractive();
static InType ReadFromFile(const std::string &filename);
static void PrintResult(const InType &input, OutType result);

private:
bool ValidationImpl() override;
bool PreProcessingImpl() override;
bool RunImpl() override;
bool PostProcessingImpl() override;
};

} // namespace gasenin_l_lex_dif
Loading
Loading