diff --git a/tasks/romanov_a_integration_rect_method/common/include/common.hpp b/tasks/romanov_a_integration_rect_method/common/include/common.hpp new file mode 100644 index 0000000000..3144fe9c08 --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/common/include/common.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +#include "task/include/task.hpp" + +namespace romanov_a_integration_rect_method { + +using InType = std::tuple, double, double, int>; +using OutType = double; +using TestType = std::tuple, double, double, int, double>; +; +using BaseTask = ppc::task::Task; + +constexpr double kEps = 1e-4; + +inline bool IsEqual(double a, double b) { + return std::abs(a - b) <= kEps; +} + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/info.json b/tasks/romanov_a_integration_rect_method/info.json new file mode 100644 index 0000000000..9e4df0d00d --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Артем", + "last_name": "Романов", + "middle_name": "Сергеевич", + "group_number": "3823Б1ФИ3", + "task_number": "1" + } +} diff --git a/tasks/romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp b/tasks/romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..594acfbc4f --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "romanov_a_integration_rect_method/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace romanov_a_integration_rect_method { + +class RomanovAIntegrationRectMethodMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit RomanovAIntegrationRectMethodMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/mpi/src/ops_mpi.cpp b/tasks/romanov_a_integration_rect_method/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..05d84d9a17 --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/mpi/src/ops_mpi.cpp @@ -0,0 +1,83 @@ +#include "romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp" + +#include + +#include + +#include "romanov_a_integration_rect_method/common/include/common.hpp" + +namespace romanov_a_integration_rect_method { + +RomanovAIntegrationRectMethodMPI::RomanovAIntegrationRectMethodMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0.0; +} + +bool RomanovAIntegrationRectMethodMPI::ValidationImpl() { + if (!IsEqual(GetOutput(), 0.0)) { + return false; + } + if (std::get<3>(GetInput()) <= 0) { + return false; + } + if (std::get<1>(GetInput()) >= std::get<2>(GetInput())) { + return false; + } + return true; +} + +bool RomanovAIntegrationRectMethodMPI::PreProcessingImpl() { + return true; +} + +bool RomanovAIntegrationRectMethodMPI::RunImpl() { + const auto f = std::get<0>(GetInput()); + + int rank = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + + int num_processes = 0; + MPI_Comm_size(MPI_COMM_WORLD, &num_processes); + + double a = 0.0; + double b = 0.0; + int n = 0; + + if (rank == 0) { + a = std::get<1>(GetInput()); + b = std::get<2>(GetInput()); + n = std::get<3>(GetInput()); + } + + MPI_Bcast(&a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); + MPI_Bcast(&b, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); + MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD); + + int block_size = (n + num_processes - 1) / num_processes; + + int left_border = rank * block_size; + int right_border = std::min(n, (rank + 1) * block_size); + + double delta_x = (b - a) / static_cast(n); + double mid = a + (delta_x * static_cast(left_border)) + (delta_x / 2.0); + + double current_result = 0.0; + + for (int i = left_border; i < right_border; ++i) { + current_result += f(mid) * delta_x; + mid += delta_x; + } + + double result = 0.0; + MPI_Allreduce(¤t_result, &result, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); + GetOutput() = result; + + return true; +} + +bool RomanovAIntegrationRectMethodMPI::PostProcessingImpl() { + return true; +} + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/report.md b/tasks/romanov_a_integration_rect_method/report.md new file mode 100644 index 0000000000..308b4788d9 --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/report.md @@ -0,0 +1,169 @@ +# Интегрирование – метод прямоугольников + +- Студент: Романов Артем Сергеевич, группа 3823Б1ФИ3 +- Технологии: SEQ | MPI +- Вариант: 19 + +## 1. Введение +Интегрирование функций является составной частью многих научных и технических задач. Поскольку аналитическое интегрирование не всегда возможно, часто используются различные методы численного интегрирования. Метод прямоугольников является одним из наиболее простых для понимания и реализации методов, идея которого заключается в приближении значения интеграла некоторой интегральной суммой. + +Целью данной работы является реализация последовательной и параллельной версий алгоритма численного нахождения значения интеграла методом прямоугольников, а также исследования эффективности их работы. + +## 2. Постановка задачи +Пусть задана функция $f(x): \mathbb{R} \to \mathbb{R},$ которая интегрируема на заданном отрезке $[a,b]$. Требуется приближённо вычислить значение определённого интеграла + +$I = \int\limits_a^b f(x)dx$ + +с помощью метода прямоугольников. + +**Входные данные:** +- функция $f(x): \mathbb{R} \to \mathbb{R}$; +- вещественные границы отрезка интегрирования $a, b$ $(a < b)$, на котором функция определена; +- натуральное число слагаемых $n$ в интегральной сумме. + +**Выходные данные:** +- $I$ - приближённое значение интеграла. + + +## 3. Базовый алгоритм (последовательный) +Метод прямоугольников заключается приближении значения определённого интеграла с помощью интегральной суммы: + +$\int_{a}^{b} f(x) dx \approx \sum_{i=1}^{n} f(x_{i}^{\*}) \cdot \Delta x_{i}$,где +- $a = x_0 < x_1 < ... < x_n = b$; +- $x_{i}^{\*} \in [x_{i-1}, x_{i}]$; +- $\Delta x_i = x_i - x_{i-1}$. + +Все $\Delta x_i$ полагаются равными $\frac{b-a}{n}$, где $n$ - число элементарных отрезков, а $x_i^*$ выбирается по-разному в зависимости от варианта метода прямоугольников. Основных вариаций три: +1. **Метод левых прямоугольников** - используется значение функции в левой границе каждого элементарного отрезка: $x_i^* = x_{i-1}$; +2. **Метод правых прямоугольников** - используется значение функции в правой границе каждого элементарного отрезка: $x_i^* = x_{i}$; +3. **Метод средних прямоугольников** - используется значение функции в середине каждого элементарного отрезка: $x_i^* = \frac{x_{i-1} + x_{i}}{2}$. + +В данной работе реализуется метод средних прямоугольников. + +Последовательный алгоритм использует представленный выше способ вычисления и может быть описан следующим псевдокодом: + +```python +  delta_x = (b - a) / n; +  mid = a + (delta_x / 2); +  result = 0.0; +  for (i = 0; i < n; i += 1) { +    result += f(mid) * delta_x; +    mid += delta_x; +  } +``` +## 4. Параллелизация +Вычисления в параллельном алгоритме организованы схожим образом: каждый процесс вычисляет значение интеграла на некотором подотрезке номеров слагаемых интегральной суммы. + +Каждый процесс обладает своей копией входных данных и в начале получает свой ранг и общее число процессов: +```cpp +  int rank = 0; +  MPI_Comm_rank(MPI_COMM_WORLD, &rank); +  +  int num_processes = 0; +  MPI_Comm_size(MPI_COMM_WORLD, &num_processes); +``` + +Затем 0-ой процесс получает входные данные и рассылает их всем остальным процессам: + +```cpp +  MPI_Bcast(&a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); +  MPI_Bcast(&b, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD); +  MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD); +``` + +На основе этих данных вычисляются левые и правые границы номеров слагаемых: +```cpp +  int block_size = (n + num_processes - 1) / num_processes; +  int left_border = rank * block_size; +  int right_border = std::min(n, (rank + 1) * block_size); +``` + +После этого схожий с последовательной версией цикл считает значение интеграла на своём подотрезке: +```cpp +  double current_result = 0.0; +  for (int i = left_border; i < right_border; ++i) { +    current_result += f(mid) * delta_x; +    mid += delta_x; +  } +``` + +Затем итоговые значения суммируются и рассылаются по всем процессам: +```cpp +  double result = 0.0; +  MPI_Allreduce(¤t_result, &result, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); +  GetOutput() = result; +``` + + +## 5. Детали реализации +Структура проекта глобально соответствует шаблону, предложенному в примерах, однако в реализации присутствует ряд особенностей: +1) Для проверки корректности результата выполнения кода в тестах используется следующая функция проверки равенства вещественных чисел: +```cpp +constexpr double kEps = 1e-4; + +inline bool IsEqual(double a, double b) { +  return std::abs(a - b) <= kEps; + +} +``` +2) Интегрируемая функция передаётся в виде объекта `std::function 0`); + - левая и правая границы интегрирования должны удовлетворять неравенству `a < b`. +## 6. Условия проведения экспериментов +- **Аппаратное обеспечение/ОС:** Intel Core i5-10400f, 6 ядер/12 логических процессоров, 32GB RAM DDR4 2667 Mhz, Ubuntu 24.04.2/Docker (под Windows 10 Home 22H2); +- **Инструменты сборки:** GCC 13.3.0, Release, Cmake 3.28.3; +- **Переменные окружения:** Не использовались (запуск тестов происходил с флагом `-np `); +- **Данные:** Тестовые данные генерировались вручную. + +## 7. Результаты + +### 7.1 Корректность +Код последовательной и параллельной версии алгоритма проверялся на некоторой группе тестов, включающей в себя различные интегрируемые функции, границы отрезков интегрирования и число слагаемых в интегральной сумме. + +В каждом тесте проверялось, что приближённое вычисленное значение интеграла отличалось от реального не более чем на $\epsilon = 10^{-4}$. + +### 7.2 Производительность + +Время, ускорение и эффективность измерялись на следующем тесте: +- $f(x) = x^3$ +- $a = -10$ +- $b = 10$ +- $n = 100'000'000$ + +В результате запуска последовательной версии алгоритма и параллельной на различном числе процессов были получены следующие данные: + +| Режим | Число процессов | Время, с | Ускорение | Эффективность | +| ----- | --------------- | -------- | --------- | ------------- | +| SEQ | 1 | 1.89 | 1.00 | N/A | +| MPI | 2 | 1.05 | 1.80 | 90.0% | +| MPI | 4 | 0.64 | 2.95 | 73.8% | +| MPI | 6 | 0.44 | 4.30 | 71.7% | +| MPI | 8 | 0.35 | 5.40 | 67.5% | +| MPI | 10 | 0.31 | 6.1 | 61.0% | +| MPI | 11 | 0.28 | 6.75 | 61.4% | +| MPI | 12 | 0.27 | 7.00 | 58.3% | +| MPI | 13 | 0.44 | 4.30 | 33.1% | +| MPI | 14 | 0.46 | 4.11 | 29.4% | +| MPI | 16 | 0.46 | 4.11 | 25.7% | +| MPI | 18 | 0.45 | 4.20 | 23.3% | +| MPI | 24 | 0.41 | 4.61 | 19.2% | + +По данной таблице видно, что достаточно эффективное ускорение присутствует при использовании от 2 до 12 процессов, что соответствует числу логических процессоров. При использовании большего числа процессов эффективность и ускорение сильно падают. + +В случае с двумя процессами код был достаточно хорошо оптимизирован компилятором, из-за чего его эффективность очень высока (при сравнении с иным числом процессов). + +Эффективность параллелизации при числе процессов до 6 обеспечивается наличием 6 физических ядер, при большем числе процессов, вероятно, влияет технология Hyper-Threading, благодаря которой каждое физическое ядро процессора представляется внутри ОС как 2 независимых. Такое возможное влияние на MPI программы упоминается в некоторых статьях в сети Интернет, например в [4]. + +В данном случае этот эффект может достигаться из-за того, что программа не имеет как таковой работы с памятью: каждому процессу для работы требуется некоторое число целочисленных и вещественных переменных, а также адрес интегрируемой функции - все эти данные могут уместиться в наборе регистров, которые присутствуют у каждого логического процессора. + +Потеря производительности при большем числе процессов чем 12 может происходить из-за частых смен контекста процессов. +## 8. Заключение +В рамках данной работы были разработаны последовательная и параллельная версии алгоритма численного вычисления определённого интеграла методом прямоугольников, а также проанализировано ускорение, которое может быть получено благодаря параллелизации кода, и его причины. + +## 9. Список литературы +1. Павлова Т. Ю. Численное интегрирование [Электронный ресурс]. – Кемеровский государственный университет, Институт фундаментальных наук. – URL: https://ifn.kemsu.ru/page_teachers/pavlova/Numerical_integration.Brief_theory.pdf (дата обращения: 12.11.2025); +2. Оболенский А., Нестеров А. Parallel Programming course. MPI (detailed API overview) [Электронный ресурс]. – Нижегородский государственный университет. – URL: https://learning-process.github.io/parallel_programming_slides/slides/03-mpi-api.pdf (дата обращения: 18.11.2025); +3. Оболенский А., Нестеров А. Parallel Programming course. Introduction [Электронный ресурс]. – Нижегородский государственный университет. – URL: https://learning-process.github.io/parallel_programming_slides/slides/01-intro.pdf (дата обращения: 18.11.2025); +4. Hyper-threading [Электронный ресурс]. – Wikipedia EN. – URL: https://en.wikipedia.org/wiki/Hyper-threading (дата обращения: 20.11.2025); +5. Гиперпоточность [Электронный ресурс]. – RuWiki – URL: https://ru.ruwiki.ru/wiki/Гиперпоточность (дата обращения: 21.11.2025). \ No newline at end of file diff --git a/tasks/romanov_a_integration_rect_method/seq/include/ops_seq.hpp b/tasks/romanov_a_integration_rect_method/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..4dd738629b --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/seq/include/ops_seq.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "romanov_a_integration_rect_method/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace romanov_a_integration_rect_method { + +class RomanovAIntegrationRectMethodSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit RomanovAIntegrationRectMethodSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; +}; + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/seq/src/ops_seq.cpp b/tasks/romanov_a_integration_rect_method/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..93ad65339f --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/seq/src/ops_seq.cpp @@ -0,0 +1,54 @@ +#include "romanov_a_integration_rect_method/seq/include/ops_seq.hpp" + +#include + +#include "romanov_a_integration_rect_method/common/include/common.hpp" + +namespace romanov_a_integration_rect_method { + +RomanovAIntegrationRectMethodSEQ::RomanovAIntegrationRectMethodSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = 0.0; +} + +bool RomanovAIntegrationRectMethodSEQ::ValidationImpl() { + if (!IsEqual(GetOutput(), 0.0)) { + return false; + } + if (std::get<3>(GetInput()) <= 0) { + return false; + } + if (std::get<1>(GetInput()) >= std::get<2>(GetInput())) { + return false; + } + return true; +} + +bool RomanovAIntegrationRectMethodSEQ::PreProcessingImpl() { + return true; +} + +bool RomanovAIntegrationRectMethodSEQ::RunImpl() { + const auto &[f, a, b, n] = GetInput(); + + double delta_x = (b - a) / static_cast(n); + double mid = a + (delta_x / 2.0); + + double result = 0.0; + + for (int i = 0; i < n; ++i) { + result += f(mid) * delta_x; + mid += delta_x; + } + + GetOutput() = result; + + return true; +} + +bool RomanovAIntegrationRectMethodSEQ::PostProcessingImpl() { + return true; +} + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/settings.json b/tasks/romanov_a_integration_rect_method/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/romanov_a_integration_rect_method/tests/.clang-tidy b/tasks/romanov_a_integration_rect_method/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/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/romanov_a_integration_rect_method/tests/functional/main.cpp b/tasks/romanov_a_integration_rect_method/tests/functional/main.cpp new file mode 100644 index 0000000000..005954ba6f --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/tests/functional/main.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "romanov_a_integration_rect_method/common/include/common.hpp" +#include "romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp" +#include "romanov_a_integration_rect_method/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace romanov_a_integration_rect_method { + +class RomanovAIntegrationRectMethodFuncTests : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + auto [f, a, b, n, result] = test_param; + + // Минус (-) от отрицательных чисел в имени gtest нельзя, функцию в имя gtest не поместить, точку (.) тоже... Как + // жить то? + return "_id" + std::to_string(static_cast(std::abs((result - a + (2.0 * b) + 4.0) * 334))) + "_n" + + std::to_string(n); + } + + protected: + void SetUp() override { + TestType params = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + + auto [f, a, b, n, result] = params; + + input_data_ = std::make_tuple(std::function(f), a, b, n); + + expected_ = result; + } + + bool CheckTestOutputData(OutType &output_data) final { + return IsEqual(static_cast(output_data), expected_); + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + InType input_data_; + double expected_ = 0.0; +}; + +namespace { + +TEST_P(RomanovAIntegrationRectMethodFuncTests, MatmulFromPic) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = { + std::make_tuple([](double x) { return x; }, -1.0, 1.0, 1, 0), + std::make_tuple([](double x) { return x; }, 0.0, 1.0, 2, 0.5), + std::make_tuple([](double x) { return x * x; }, 0.0, 1.0, 1000000 + 1, 1.0 / 3.0), + std::make_tuple([](double x) { return (std::sin(x) * x) - std::sqrt(x); }, 0.0, std::numbers::pi, 1000000 + 2, + -0.570626), + std::make_tuple([](double x) { return std::exp(x) - x; }, -1.0, 3.0, 1000000 + 3, + -4.0 - (1.0 / std::numbers::e) + std::exp(3.0)), + std::make_tuple([](double x) { return std::cos(x) * std::sqrt(x); }, 0.0, std::numbers::pi, 1000000 + 4, + -0.894831)}; + +const auto kTestTasksList = std::tuple_cat(ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_romanov_a_integration_rect_method), + ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_romanov_a_integration_rect_method)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = + RomanovAIntegrationRectMethodFuncTests::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(PicMatrixTests, RomanovAIntegrationRectMethodFuncTests, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace romanov_a_integration_rect_method diff --git a/tasks/romanov_a_integration_rect_method/tests/performance/main.cpp b/tasks/romanov_a_integration_rect_method/tests/performance/main.cpp new file mode 100644 index 0000000000..1cedce5a13 --- /dev/null +++ b/tasks/romanov_a_integration_rect_method/tests/performance/main.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include +#include + +#include "romanov_a_integration_rect_method/common/include/common.hpp" +#include "romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp" +#include "romanov_a_integration_rect_method/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace romanov_a_integration_rect_method { + +class RomanovAIntegrationRectMethodPerfTests : public ppc::util::BaseRunPerfTests { + const int kCount_ = 100'000'000; + InType input_data_; + + void SetUp() override { + auto f = std::function([](double x) { return std::pow(x, 3); }); + double a = -10.0; + double b = 10.0; + int n = kCount_; + + input_data_ = std::make_tuple(f, a, b, n); + } + + bool CheckTestOutputData(OutType &output_data) final { + return IsEqual(static_cast(output_data), 0.0); + } + + InType GetTestInputData() final { + return input_data_; + } +}; + +TEST_P(RomanovAIntegrationRectMethodPerfTests, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_romanov_a_integration_rect_method); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = RomanovAIntegrationRectMethodPerfTests::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, RomanovAIntegrationRectMethodPerfTests, kGtestValues, kPerfTestName); + +} // namespace romanov_a_integration_rect_method