Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
94 changes: 94 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,94 @@
#include "gasenin_l_lex_dif/mpi/include/ops_mpi.hpp"

#include <mpi.h>

#include <algorithm>
#include <cstdint>

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

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, size;
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);

int local_result = 0;
size_t local_diff_pos = total_len;

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) {
local_diff_pos = i;
local_result = (c1 < c2) ? -1 : 1;
break;
}
}

uint64_t 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);

size_t global_min_pos = static_cast<size_t>(global_min_pos_64);

int result_for_sum = 0;
if (local_diff_pos == global_min_pos) {
result_for_sum = local_result;
}

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 = -1;
} else if (str1.length() > str2.length()) {
final_result = 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
27 changes: 27 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,27 @@
#pragma once
#include <cstddef>

#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