Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <cmath>
#include <functional>
#include <tuple>

#include "task/include/task.hpp"

namespace romanov_a_integration_rect_method {

using InType = std::tuple<std::function<double(double)>, double, double, int>;
using OutType = double;
using TestType = std::tuple<std::function<double(double)>, double, double, int, double>;
;
using BaseTask = ppc::task::Task<InType, OutType>;

constexpr double kEps = 1e-4;

inline bool IsEqual(double a, double b) {
Comment thread
allnes marked this conversation as resolved.
return std::abs(a - b) <= kEps;
}

} // namespace romanov_a_integration_rect_method
9 changes: 9 additions & 0 deletions tasks/romanov_a_integration_rect_method/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"
}
}
22 changes: 22 additions & 0 deletions tasks/romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp
Original file line number Diff line number Diff line change
@@ -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
83 changes: 83 additions & 0 deletions tasks/romanov_a_integration_rect_method/mpi/src/ops_mpi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "romanov_a_integration_rect_method/mpi/include/ops_mpi.hpp"

#include <mpi.h>

#include <algorithm>

#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)) {
Comment thread
allnes marked this conversation as resolved.
return false;
}
if (std::get<3>(GetInput()) <= 0) {
return false;
}
if (std::get<1>(GetInput()) >= std::get<2>(GetInput())) {
return false;
Comment thread
allnes marked this conversation as resolved.
}
return true;
}

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

bool RomanovAIntegrationRectMethodMPI::RunImpl() {
Comment thread
allnes marked this conversation as resolved.
const auto f = std::get<0>(GetInput());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the data scattering across the ranks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Во время занятия 20.11.2025 Александр Нестеров сказал, что в задачах интегрирования объект требуемой функции std::function можно напрямую доставать из 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<double>(n);
double mid = a + (delta_x * static_cast<double>(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(&current_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
169 changes: 169 additions & 0 deletions tasks/romanov_a_integration_rect_method/report.md
Original file line number Diff line number Diff line change
@@ -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(&current_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<double(double)`. В тестах такие объекты задаются в виде лямбда-функций;
3) Перед выполнением вычислений проводится проверка корректности передаваемых параметров:
- количество слагаемых `n` должно быть натуральным числом (`n > 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 <x>`);
- **Данные:** Тестовые данные генерировались вручную.

## 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).
22 changes: 22 additions & 0 deletions tasks/romanov_a_integration_rect_method/seq/include/ops_seq.hpp
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions tasks/romanov_a_integration_rect_method/seq/src/ops_seq.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#include "romanov_a_integration_rect_method/seq/include/ops_seq.hpp"

#include <cmath>

#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;
Comment thread
allnes marked this conversation as resolved.
}
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<double>(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
Loading
Loading