отчет лаба 2 Гаранин
.docx
ФЕДЕРАЛЬНОЕ
АГЕНСТВО ВОЗДУШНОГО ТРАНСПОРТА
(РОСАВИАЦИЯ)
ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ОБРАЗОВАНИЯ
«МОСКОВСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ ГРАЖДАНСКОЙ АВИАЦИИ» (МГТУ ГА)
Кафедра вычислительных машин, комплексов, сетей и систем.
Лабораторная работа защищена с оценкой ____________________
____________________
(подпись преподавателя, дата)
ЛАБОРАТОРНАЯ РАБОТА №2
по дисциплине «Методы визуального и параллельного программирования».
Тема: «Методы оценки ускорения выполнения программ на многопроцессорных системах.»
Выполнила студент группы ИС221
Магальник Екатерина Борисовна
Руководитель: Гаранин Сергей Александрович
МОСКВА – 2025
Вариант 1. Демонстрация закона Густавсона–Барсиса в задачах с параллелизмом.
Цель работы:
Изучить особенности масштабируемого параллелизма на примере закона Густавсона–Барсиса. Освоить принципы оценки ускорения вычислений при увеличении числа потоков при условии фиксированной последовательной части и увеличивающейся параллельной.
Теоретические сведения:
Закон Густавсона–Барсиса описывает, как можно эффективно использовать увеличивающееся количество процессоров, если параллельную часть задачи масштабировать пропорционально количеству потоков, при фиксированной последовательной части.
Формула ускорения: S(N)=N−α⋅(N−1),
где: S(N) — ускорение, N — количество потоков, α — доля последовательной части (не распараллеливаемая): α=Tпосл/(Tпосл+Tпарал)
Постановка задачи:
Реализовать программу, в которой присутствует: последовательная часть фиксированной длительности (например, задержка в 300 мс), параллельная часть в виде суммирования элементов большого массива, размер которого масштабируется пропорционально числу потоков.
Провести измерения общего времени выполнения программы при 1, 2, 4, 8 потоках.
Вычислить для каждого эксперимента:
долю последовательной части α,
ускорение S(N) по формуле Густавсона–Барсиса.
Построить таблицу и график зависимости S(N) от числа потоков.
Сделать выводы о масштабируемости задачи.
Требования:
Для каждой конфигурации потоков программа должна:
выполнять фиксированную последовательную часть (например, 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 по отношению к общему времени работы на одном процессоре для задачи базового размера.
