Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

отчет лаба 2 Гаранин

.docx
Скачиваний:
0
Добавлен:
11.02.2026
Размер:
123.24 Кб
Скачать

ФЕДЕРАЛЬНОЕ АГЕНСТВО ВОЗДУШНОГО ТРАНСПОРТА

(РОСАВИАЦИЯ)

ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ОБРАЗОВАНИЯ

«МОСКОВСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ ГРАЖДАНСКОЙ АВИАЦИИ» (МГТУ ГА)

Кафедра вычислительных машин, комплексов, сетей и систем.

Лабораторная работа защищена с оценкой ____________________

____________________

(подпись преподавателя, дата)

ЛАБОРАТОРНАЯ РАБОТА №2

по дисциплине «Методы визуального и параллельного программирования».

Тема: «Методы оценки ускорения выполнения программ на многопроцессорных системах.»

Выполнила студент группы ИС221

Магальник Екатерина Борисовна

Руководитель: Гаранин Сергей Александрович

МОСКВА – 2025

Вариант 1. Демонстрация закона Густавсона–Барсиса в задачах с параллелизмом.

Цель работы:

Изучить особенности масштабируемого параллелизма на примере закона Густавсона–Барсиса. Освоить принципы оценки ускорения вычислений при увеличении числа потоков при условии фиксированной последовательной части и увеличивающейся параллельной.

Теоретические сведения:

Закон Густавсона–Барсиса описывает, как можно эффективно использовать увеличивающееся количество процессоров, если параллельную часть задачи масштабировать пропорционально количеству потоков, при фиксированной последовательной части.

Формула ускорения: S(N)=N−α⋅(N−1),

где: S(N) — ускорение, N — количество потоков, α — доля последовательной части (не распараллеливаемая): α=Tпосл/(Tпосл+Tпарал)

Постановка задачи:

  1. Реализовать программу, в которой присутствует:  последовательная часть фиксированной длительности (например, задержка в 300 мс),  параллельная часть в виде суммирования элементов большого массива, размер которого масштабируется пропорционально числу потоков.

  2. Провести измерения общего времени выполнения программы при 1, 2, 4, 8 потоках.

  3. Вычислить для каждого эксперимента:

  • долю последовательной части α,

  • ускорение S(N) по формуле Густавсона–Барсиса.

  1. Построить таблицу и график зависимости S(N) от числа потоков.

  2. Сделать выводы о масштабируемости задачи.

Требования:

  1. Для каждой конфигурации потоков программа должна:

  • выполнять фиксированную последовательную часть (например, sleep_for(300ms)),

  • выполнять параллельную часть (например, суммировать элементы массива из единиц длиной N * 50'000'000 элементов),

  • измерять общее время выполнения,

  • вычислять α и ускорение.

2. Отчёт должен содержать таблицу замеров:

Потоки

Время выполнения (сек)

Доля последовательной части

Ускорение

Ход работы:

Рис. 1. Алгоритм функции calibrate_single_thread_performance()

Рис. 2. Алгоритм функции sum_array_chunk(…)

Листинг:

#include <iostream>

#include <vector>

#include <thread>

#include <chrono>

#include <numeric> // For std::accumulate (optional, for single thread sum)

#include <atomic>

#include <iomanip> // For std::fixed and std::setprecision

#include <cmath> // For std::round

// Константы

const std::chrono::milliseconds SEQUENTIAL_PART_DURATION(300);

const long long BASE_CHUNK_SIZE = 50000000; // 50 миллионов

// Глобальная переменная для хранения времени выполнения базового блока на 1 потоке

std::chrono::duration<double> time_per_base_chunk_single_thread;

// Функция, выполняемая каждым потоком для суммирования своей части массива

void sum_array_chunk(const std::vector<long long>& arr,

long long start_index,

long long end_index,

std::atomic<long long>& total_sum) {

long long local_sum = 0;

for (long long i = start_index; i < end_index; ++i) {

local_sum += arr[i];

}

total_sum += local_sum;

}

// Функция для калибровки: измеряет время суммирования BASE_CHUNK_SIZE на 1 потоке

void calibrate_single_thread_performance() {

std::cout << "Калибровка: измерение производительности одного потока..." << std::endl;

std::vector<long long> calibration_arr(BASE_CHUNK_SIZE, 1);

std::atomic<long long> sum(0);

auto start_time = std::chrono::high_resolution_clock::now();

// Суммируем на одном потоке

sum_array_chunk(calibration_arr, 0, BASE_CHUNK_SIZE, sum);

// Или можно использовать std::accumulate для простоты, если это именно 1 поток

// sum = std::accumulate(calibration_arr.begin(), calibration_arr.end(), 0LL);

auto end_time = std::chrono::high_resolution_clock::now();

time_per_base_chunk_single_thread = end_time - start_time;

std::cout << "Калибровка завершена. Время суммирования " << BASE_CHUNK_SIZE

<< " элементов на 1 потоке: "

<< time_per_base_chunk_single_thread.count() << " сек." << std::endl << std::endl;

}

int main() {

std::locale::global(std::locale("")); // Для корректного вывода русских букв в некоторых консолях

calibrate_single_thread_performance();

std::cout << std::fixed << std::setprecision(4);

std::cout << "---------------------------------------------------------------------------" << std::endl;

std::cout << "| Потоки | Время выполнения (сек) | Доля послед. части | Ускорение (S(N)) |" << std::endl;

std::cout << "---------------------------------------------------------------------------" << std::endl;

std::vector<int> num_threads_list = { 1, 2, 4, 8 };

for (int num_threads : num_threads_list) {

long long current_array_size = static_cast<long long>(num_threads) * BASE_CHUNK_SIZE;

std::vector<long long> arr(current_array_size, 1); // Заполняем единицами

std::atomic<long long> total_sum(0);

// --- Начало измерения общего времени ---

auto overall_start_time = std::chrono::high_resolution_clock::now();

// 1. Последовательная часть

std::this_thread::sleep_for(SEQUENTIAL_PART_DURATION);

// 2. Параллельная часть

std::vector<std::thread> threads;

long long elements_per_thread = current_array_size / num_threads;

long long start_index = 0;

for (int i = 0; i < num_threads; ++i) {

long long end_index = start_index + elements_per_thread;

if (i == num_threads - 1) { // Последний поток забирает остаток

end_index = current_array_size;

}

threads.emplace_back(sum_array_chunk, std::ref(arr), start_index, end_index, std::ref(total_sum));

start_index = end_index;

}

for (auto& t : threads) {

if (t.joinable()) {

t.join();

}

}

// --- Конец измерения общего времени ---

auto overall_end_time = std::chrono::high_resolution_clock::now();

std::chrono::duration<double> total_time_measured_duration = overall_end_time - overall_start_time;

double total_time_measured_sec = total_time_measured_duration.count();

// Проверка суммы (опционально)

// std::cout << "Проверочная сумма для " << num_threads << " потоков: " << total_sum << " (ожидалось: " << current_array_size << ")" << std::endl;

// 3. Вычисления

double sequential_part_duration_sec = std::chrono::duration<double>(SEQUENTIAL_PART_DURATION).count();

// Доля последовательной части α

double alpha = sequential_part_duration_sec / total_time_measured_sec;

// Ускорение S(N) по Густавсону–Барсису

// S(N) = (время_послед_части_на_N + время_паралл_части_на_1_для_размера_N) / общее_время_на_N

// время_паралл_части_на_1_для_размера_N = N * time_per_base_chunk_single_thread

double time_parallel_part_single_thread_scaled_sec = num_threads * time_per_base_chunk_single_thread.count();

double sequential_equivalent_time_for_scaled_problem_sec = sequential_part_duration_sec + time_parallel_part_single_thread_scaled_sec;

double speedup_gustafson = sequential_equivalent_time_for_scaled_problem_sec / total_time_measured_sec;

std::cout << "| " << std::setw(6) << num_threads << " | "

<< std::setw(22) << total_time_measured_sec << " | "

<< std::setw(23) << alpha << " | "

<< std::setw(16) << speedup_gustafson << " |" << std::endl;

}

std::cout << "---------------------------------------------------------------------------" << std::endl;

return 0;

}

Результат работы программы:

Рис. 3. Результат выполнения программы.

Вывод:

1. Анализ α (доля последовательной части):

⦁ Обратим внимание, как меняется α = T_послед / T_общее(N). В модели Густавсона T_общее(N) должно расти медленнее, чем N (если параллельная часть хорошо масштабируется). Если α уменьшается с ростом N, это означает, что относительное влияние фиксированной последовательной части снижается по мере увеличения общей работы и числа процессоров.

⦁ Если α остается большим, это указывает на то, что последовательная часть доминирует, и масштабируемость будет ограничена (ближе к закону Амдала для фиксированного размера задачи).

2. Анализ S(N) (ускорение по Густавсону-Барсису):

⦁ В идеальном случае для задачи, хорошо масштабируемой по Густавсону (где объем параллельной работы растет пропорционально N), ускорение S(N) должно быть близко к N.

⦁ На практике S(N) будет меньше N из-за:

⦁ Накладных расходов на создание и синхронизацию потоков.

⦁ Ограничений пропускной способности памяти (все потоки конкурируют за доступ к памяти).

⦁ Неидеального распределения нагрузки (хотя в данном случае оно довольно равномерное).

⦁ Сохраняющейся фиксированной последовательной части.

⦁ Если S(N) растет почти линейно с N (например, S(4) ≈ 3.8, S(8) ≈ 7.5), это говорит о хорошей масштабируемости задачи в рамках модели Густавсона (масштабируемый размер задачи).

⦁ Если рост S(N) замедляется значительно (например, S(4) ≈ 2.5, S(8) ≈ 3.0), это указывает на проблемы с масштабируемостью, даже при увеличении размера задачи. Возможно, накладные расходы становятся слишком велики или другие узкие места (например, память) начинают доминировать.

3. Сравнение с теорией:

⦁ Закон Густавсона-Барсиса: S(N) = f_s + N * (1 - f_s), где f_s - это доля времени, затрачиваемого на последовательные операции в однопоточном выполнении масштабированной задачи.

⦁ В нашей реализации мы рассчитывали S(N) = (T_seq_fixed + N * T_p_base_1_thread) / T_measured_total_N_threads.

⦁ f_s можно оценить как T_seq_fixed / (T_seq_fixed + N * T_p_base_1_thread) для каждого N. Тогда теоретическое S_theoretical(N) = (1 - f_s_N) * N + f_s_N. Интересно сравнить полученные S(N) с этим теоретическим значением, учитывая, что f_s здесь само зависит от N.

⦁ Более простой подход для Густавсона: если s - это доля времени, которую программа тратит на последовательную часть, независимо от числа процессоров и размера задачи, а p = 1-s - доля параллелизуемой части, то S(N) = s + N*p. В нашем случае, s - это время SEQUENTIAL_PART_DURATION по отношению к общему времени работы на одном процессоре для задачи базового размера.