1 / 1302_3_1
.pdf
МИНОБРНАУКИ РОССИИ САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ ЭЛЕКТРОТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ «ЛЭТИ» ИМ. В.И. УЛЬЯНОВА (ЛЕНИНА) Кафедра САПР
ОТЧЕТ по лабораторной работе №1
по дисциплине «Архитектура параллельных вычислительных систем» ТЕМА: «Умножение матрицы на вектор и матрицы на матрицу на системах с общей памятью»
Вариант 3
Студенты гр. 1302 |
Харитонов А.А.. |
Наволоцкий И.Р.
Солончак И.П.
Преподаватель |
Костичев С.В. |
Санкт-Петербург
2025
Цель работы
Получить знания о конструировании простых параллельных алгоритмов на системах с общей памятью; получить общее представление о масштабируемости задач; практическое освоение основных директив OpenMP и mpi; способах распределения вычислений между потоками; способах распределения вычислений итерационных циклов между потоками
1.Задание на лабораторную работу
1.1. Реализовать умножение матрицы на матрицу с использованием директивы распараллеливания параметрических циклов #pragma omp for и с использованием “ручного” задания работ (распараллеливания циклов без директивы for).
1.2.Написать и отладить программы на языке С++, реализующие разработанные алгоритмы последовательных и параллельных вычислений с использованием библиотек OpenMP и mpi.
1.3.Запустить программы для следующих значений размерности матрицы
ивектора: 10, 100, 500, 1000, 5000, 10000
1.4.Оценить размерности матрицы и вектора, при которых эффективнее использовать алгоритмы последовательного и параллельного вычислений для разного числа потоков (по крайней мере, для меньшего, равного и большего, чем число процессоров). Под эффективностью понимается время работы программы на матрице.
2. Программное и аппаратное окружение при выполнении работы
При выполнении лабораторной работы использовался ноутбук Apple MacBook Air с чипом M1, содержащим 8-ядерный CPU и 7-ядерный GPU, а также 8 Гб оперативной памяти. Работа выполнялась под управлением macOS Sequoia 15.7.1.
Среда разработки: Visual Studio Code 1.105.1, система сборки: CMake, управляемый через расширение CMake Tools в VS Code.
2
3.Формализация задачи
3.1. Описание алгоритма последовательного вычисления произведения матрицы на матрицу
1.Программа запускается и инициализирует генератор случайных чисел.
2.В коде определён статический список размеров матриц для тестирования (10, 100, 500, 1000, 5000, 10000). Программа итерационно проходит по каждому из этих размеров.
3.Для каждого размера создаются три квадратные матрицы (A, B, result) с элементами типа double.
4.Матрицы A и B инициализируются случайными вещественными числами в диапазоне от 0.0 до 9.9.
5.Запускается таймер std::chrono для замера времени выполнения.
6.Вызывается функция последовательного умножения. Для повышения производительности и более эффективного использования кэш-памяти процессора, алгоритм использует оптимизированный порядок вложенных циклов (i, k, j):
•Внешний цикл перебирает строки i результирующей
матрицы.
•Средний цикл (k) итерирует по элементам строки i из матрицы A и столбца k из матрицы B.
•Внутренний цикл (j) выполняет суммирование произведений A[i][k] * B[k][j] для всей i-й строки результирующей матрицы.
•Такой порядок обеспечивает более последовательный доступ к данным в памяти, что значительно сокращает количество «промахов» мимо кэша и ускоряет вычисления.
7.Таймер останавливается, и измеренное время выполнения сохраняется для последующего вывода.
3
8. После завершения всех вычислений для данного размера, программа выводит время выполнения в секундах в общую таблицу результатов.
3.2. Описание |
алгоритмов |
параллельного |
вычисления |
произведения матрицы на матрицу с использованием OpenMP
Для параллельного выполнения были реализованы два подхода, основанных на директивах стандарта OpenMP для систем с общей памятью.
3.2.1.Параллельный алгоритм с использованием директивы for
1.Начальные шаги (запуск, перебор размеров, создание и инициализация матриц) аналогичны последовательному алгоритму. В коде также определён список для тестирования разного количества потоков (2, 4, 8, 16).
2.Для каждого количества потоков запускается таймер
std::chrono.
3.Вызывается функция параллельного умножения, которой передаётся количество потоков для использования.
4.Перед внешним циклом (перебирающим строки i) используется директива #pragma omp parallel for. Эта директива автоматически распределяет итерации внешнего цикла (то есть, вычисление строк результирующей матрицы) между созданной командой потоков.
5.Потоки выполняют вычисления параллельно, работая с общими данными матриц A и B (только чтение) и записывая результаты в разные области общей матрицы result (каждый поток пишет в назначенные ему строки), что исключает конфликты доступа.
6.Внутри каждого потока вычисления производятся с использованием того же кэш-эффективного порядка циклов (i, k, j), что
4
и в последовательной версии. Это позволяет каждому ядру процессора максимально эффективно использовать свою локальную кэш-память.
7.После завершения всех итераций цикла неявная барьерная синхронизация гарантирует, что все потоки завершили свою работу, прежде чем программа продолжит выполнение.
8.Таймер останавливается, и время выполнения сохраняется.
9.Результат заносится в таблицу производительности.
3.2.2Параллельный алгоритм с «ручным» распределением работ (директива sections)
Данный метод иллюстрирует ручное разделение работы между потоками и в коде реализован для фиксированного числа потоков (4).
1.Начальные шаги аналогичны предыдущим алгоритмам.
2.Запускается таймер std::chrono.
3.Вызывается функция, использующая «ручное» распараллеливание.
4.Используется директива #pragma omp parallel sections, которая создает команду потоков.
5.Внутри этой директивы находятся четыре блока, каждый из которых помечен директивой #pragma omp section. Каждая такая секция назначается одному свободному потоку из команды.
6.Работа по вычислению строк результирующей матрицы разделена статически и вручную. Внутри каждого блока вычисление производится с использованием кэш-эффективного порядка циклов (i, k, j):
•Первая секция: вычисляет строки с 0 до size / 4.
•Вторая секция: вычисляет строки с size / 4 до size / 2.
• Третья секция: вычисляет строки с size / 2 до 3 * size / 4.
5
• Четвертая секция: вычисляет строки с 3 * size / 4 до size.
7.После того как все секции будут выполнены, потоки синхронизируются, и выполнение программы продолжается.
8.Таймер останавливается, и время выполнения заносится в
таблицу.
4.Примеры работы программы
Исходный код программ приведён в приложении А. Блок-схемы приведены в приложении Б.
Перед анализом результатов важно сделать пояснение касательно реализации алгоритма умножения. Изначальная версия кода была написана в соответствии с примером, приведённым в тексте лабораторной работы, где использовался классический порядок вложенных циклов i, j, k.
Однако при проведении предварительных тестов на матрицах большого размера (начиная с 5000x5000) было выявлено, что время выполнения программы было чрезмерно долгим, десятки минут на один тест (рисунок 1). Это делало невозможным проведение полного набора экспериментов в разумные сроки.
Анализ проблемы показал, что причина такой низкой производительности кроется в неэффективном использовании иерархии памяти современного процессора. При порядке циклов i, j, k доступ к элементам матрицы B (b[k][j]) происходит по столбцам. Так как в языке C++ двумерные массивы хранятся в памяти построчно, такой доступ заставляет процессор постоянно «прыгать» по разным участкам оперативной памяти. Это приводит к огромному количеству промахов кэша, когда необходимые данные не находятся в быстрой кэш-памяти процессора и их приходится подгружать из медленной основной памяти.
6
Для решения этой проблемы была применена стандартная и крайне эффективная техника оптимизации – изменение порядка циклов на i, k, j. При таком порядке доступ к данным обеих исходных матриц (a[i][k] и b[k][j]) становится последовательным (по строкам). Это позволяет процессору эффективно предсказывать и загружать данные в кэш, минимизируя простои и значительно ускоряя вычисления.
Данная оптимизация позволила сократить время выполнения тестов в несколько раз и успешно провести все замеры, представленные ниже. Таким образом, все последующие результаты и выводы основаны на работе кэшэффективной реализации алгоритма с порядком циклов i, k, j.
Рисунок 1 – Время работы программы при использовании неоптимизированного алгоритма
4.1.Демонстрация работы алгоритма
При запуске программа в первую очередь выполняет умножение двух случайно сгенерированных матриц размером 10x10. Как видно на рисунке 2, программа печатает исходные матрицы «Matrix A (random)» и «Matrix B
7
(random)», после чего выводит результат их перемножения «Matrix C = A * B (Result)». Этот шаг служит для визуального подтверждения того, что алгоритм умножения реализован корректно, прежде чем переходить к масштабным замерам производительности.
Рисунок 2 – Результат работы программы при размерах матрицы 10x10 и последовательных вычислениях
4.2.Анализ результатов производительности
После верификации программа выполняет серию тестов для измерения времени умножения матриц разных размеров (от 10x10 до 10000x10000) с использованием последовательного и двух параллельных (на базе
8
OpenMP for и sections) алгоритмов. Результаты замеров сведены в общую таблицу, представленную на рисунке 3.
Рисунок 3 – Итоговая сводная таблица производительности для различных алгоритмов умножения матриц
4.2.1. Влияние размера матриц на время выполнения
Как и ожидалось теоретически, время выполнения алгоритма умножения матриц находится в кубической зависимости O(n3) от их размера n. Это наглядно демонстрируют результаты последовательного вычисления:
•При увеличении размера в 10 раз (от 100x100 до 1000x1000) время выполнения выросло примерно в 628 раз (с 0.00032 с до 0.20113 с), что близко
ктеоретическому росту в 1000 раз (10³).
•При увеличении размера в 2 раза (от 5000x5000 до 10000x10000) время выполнения последовательной версии выросло почти в 10 раз (с 33.5 с до 328.9 с), что близко к теоретическому увеличению в 8 раз (2³).
9
Это подтверждает, что для больших матриц сложность алгоритма является доминирующим фактором, и необходимость в параллельных вычислениях становится критической.
4.2.2. Эффективность параллельных вычислений
Сравнение последовательного и параллельных методов показывает однозначное преимущество распараллеливания для всех нетривиальных размеров матриц.
•На малых размерах (10x10, 100x100) выигрыш незначителен или отсутствует, так как накладные расходы на создание и синхронизацию потоков сопоставимы со временем самих вычислений.
•Начиная с размера 500x500, параллельные версии становятся значительно быстрее.
•Для матрицы 10000x10000 наилучший параллельный результат (144.35 с на 4 потоках) оказался примерно в 2.28 раза быстрее последовательной версии (328.94 с), что демонстрирует существенное ускорение.
4.2.3. Влияние количества потоков и масштабируемость
Анализ времени выполнения параллельного алгоритма на базе директивы for при разном количестве потоков выявляет важную закономерность – эффективность масштабирования не является линейной.
•Хорошая масштабируемость: на размерах 500x500 и 1000x1000 наблюдается хороший прирост производительности при увеличении числа потоков с 2 до 4 и с 4 до 8. Например, для 1000x1000 время сократилось с 0.11
с(2 потока) до 0.049 с (8 потоков).
•Насыщение и деградация производительности: на самых больших размерах (5000x5000 и 10000x10000) эта тенденция меняется. Оптимальное количество потоков достигается на 4 или 8. Дальнейшее увеличение числа потоков (до 16) не дает ускорения и даже может приводить
10
