Потребность решения сложных прикладных задач с большим объемом вычислений и принципиальная ограниченность максимального быстродействия «классических» – по схеме фон Неймана – ЭВМ привели к появлению многопроцессорных вычислительных систем (МВС) или суперкомпьютеров.
Широкое распространение параллельные вычисления приобрели с переходом компьютерной индустрии на массовый выпуск многоядерных процессоров с векторными расширениями. В настоящие время практически все устройства – от карманных гаджетов и до самых мощных суперкомпьютеров – оснащены многоядерными процессорами. И если вы пишите последовательную программу, не применив распределение работы между разными ядрами центрального процессора и не проведя векторизацию, то вы используете только часть вычислительных возможностей центрального процессора.
Пройдя этот курс, вы познакомитесь с основными архитектурами МВС, с двумя стандартами (OpenMP и MPI), позволяющими писать параллельные программы для систем с общей и распределенной памятью. На простых примерах будут разобраны основные конструкции и способы распределения работы. Выполнение практических заданий позволит вам приобрести практические навыки создания параллельных программ. Курс будет интересен всем, кто занимается программированием.
Для участия в курсе слушателю необходимо иметь базовые знания по программированию с использованием С/С++.
Курс состоит из 9 недель. Каждая неделя курса содержит видеолекции, а также проверочные задания. Сертификат получают слушатели, набравшие более 80 % от максимально возможного количества баллов. При этом итоговый результат, представленный как 100 %, складывается из следующих составляющих: тесты 1–5 недели дают 4 %, тесты 6–9 недели дают 5 %, все практические задания дают 10 %, кроме итогового практического задания по OpenMP, которое дает 20 %.
From the lesson
Векторные вычисления с помощью OpenMP 4.0
Приветствуем вас на четвертой неделе курса! На этой недели мы разберемся с тем, что такое векторизация и зачем она нужна. Рассмотрим, как можно векторизовать код для современных процессоров, и познакомимся с новыми возможностями стандарта OpenMP 4.0.
Николай Николаевич Богословский (Nikolay N. Bogoslovskiy)
Кандидат физико-математических наук, доцент (Сandidate of Physics and Mathematics, Associate Professor) Кафедра вычислительной математики и компьютерного моделирования ММФ (Department of Calculus Mathematics and Computer Modelling, Mechanics and Mathematics Faculty)
Евгений Александрович Данилкин (Evgeniy A. Danilkin)
Кандидат физико-математических наук, доцент (Сandidate of Physics and Mathematics, Associate Professor) Кафедра вычислительной математики и компьютерного моделирования ММФ (Department of Calculus Mathematics and Computer Modelling, Mechanics and Mathematics Faculty)
[МУЗЫКА]
[МУЗЫКА]
Здравствуйте!
В этой лекции, как я и обещал,
мы познакомимся с несколькими способами заставить нашу программу использовать
векторные инструкции для того, чтобы она выполнялась быстрее.
Итак, получить желанные векторные инструкции можно несколькими способами.
Изобразим эти способы в виде следующей схемы: слева написаны
способы векторизации, а справа сложность их использования.
Давайте кратко разберемся с каждым из способов.
Если мы опытные программисты и можем писать на чистом ассемблере,
то мы сразу напишем программу с использованием нужных инструкций,
и используем все возможности процессора.
Такой путь даст нам практически 100-процентную уверенность в использовании
наиболее эффективных и быстрых машинных команд в нашем коде,
а значит мы будем использовать все возможности современного процессора.
Самая простая функция сложения векторов
из 4-х чисел типа float на ассемблере выглядит следующим образом.
В данной функции я воспользовался инструкцией addps,
которая позволяет выполнять сложение 2-х векторов, загруженных в векторные
регистры: xmm0 и xmm1.
Полный исходный код этой и последующих программ вы можете взять в
дополнительных материалах к лекции.
Вот только такая программа будет заточена под конкретный набор
инструкций (в моем примере — под набор инструкций SSE),
а соответственно, и под конкретные процессоры.
А прогресс, как вы знаете, не стоит на месте.
Поэтому выход нового процессора и новых, соответственно,
наборов инструкций потребует переработки кода и, конечно же, новых трудозатрат.
Также на ассемблере тяжеловато писать,
нужно хорошо представлять себе структуру программы.
Поэтому хотелось бы найти более простой способ использования векторных инструкций.
И на следующей ступеньке появляются intrinsic функции.
Это уже не чистый ассемблер.
Его проще использовать.
Скажем, простой цикл, в котором складывают 2 массива, будет выглядеть так.
В данном случае мы использовали AVX intrinsic функции.
Таким образом, мы гарантируем генерацию соответствующих AVX инструкций.
Но в данном случае мы все равно привязаны к конкретному набору инструкций и,
как следствие, к конкретным процессорам.
Рано или поздно этот код все равно снова придется переписывать.
Рассмотрим следующий способ — это SIMD intrinsic класс.
Это интересная вещь, представляющая следующий уровень абстракции.
Перепишем тот же пример со сложением векторов с использованием
intrinsic классов.
Как вы видите, код стал более простым.
В этом случае нам уже не нужно знать, какие функции использовать.
Разработчику достаточно создать данные нужного класса.
В этом примере F64 означает тип float, размер 64 бита,
а vec4 говорит об использовании инструкций AVX.
Но использовать этот код в будущем мы не сможем.
Рано или поздно и его придется снова переписывать.
И так будет всегда, пока мы в нашей программе явно прописываем,
какие инструкции использовать: будь то чистый ассемблер,
intrinsic функции или SIMD intrinsic классы.
Поэтому разумным решением является
использование компилятора для решения подобных задач.
Используя компилятор, пересобирая наш код, мы сможем создавать бинарники под нужную
нам архитектуру, какой бы она ни была, и использовать последние наборы инструкций.
При этом нам нужно убедиться,
что компилятор в состоянии сам векторизовать код.
Пока мы двигались по схеме снизу вверх, обсуждая сложные пути векторизации кода.
Поговорим о более простых способах.
Как вы думаете, какой самый простой способ векторизовать ваш код?
Вы удивитесь, но, как и в жизни,
самый простой способ — это на кого-нибудь переложить свою работу.
В данном случае всю ответственность по векторизации необходимо переложить на
компилятор и наслаждаться жизнью.
Многие современные компиляторы поддерживают автовекторизацию.
Они способны решить все те вопросы, которые я озвучил в прошлой лекции,
и сгенерировать векторный код.
Во многих компиляторах при выборе опции оптимизации O2 происходит
автовекторизация.
Например, современный компилятор может векторизовать такой код.
Если бы мы пытались создать аналог кода на intrinsic функциях,
гарантируя векторизацию, то получилось бы что-нибудь подобное.
Как видите,
для ручной векторизации потребовалось приложить достаточное количество усилий,
а для автовекторизации достаточно воспользоваться опцией компилятора.
Хорошо, когда компилятор умеет делать это за вас, но, к сожалению,
не все так просто.
Каким бы умным ни был компилятор, — а с каждым годом они становятся все умней
и умней, — существует множество случаев,
когда компилятор не в состоянии векторизовать код.
В этом случае мы должны помочь компилятору с помощью определенных подсказок.
Мы должны сообщить ему дополнительную информацию об алгоритме или о данных,
и потом он сможет векторизовать этот код уже сам.
Чаще такими подсказками являются специальные директивы.
Например, #pragma ivdep подскажет,
что в цикле нет зависимости, а директива #pragma vector
always позволит не обращать внимание на политику эффективности векторизации.
Часто, если компилятор считает, что векторизовать цикл неэффективно, скажем,
из-за доступа к памяти, он этого не делает.
Но эти директивы из разряда «возможно помогут».
Если компилятор уверен, что зависимости есть, то цикл он так и не будет
векторизовать, даже если имеется директива #pragma ivdep.
Поэтому выделим еще один класс, основанный на директивах, но других принципах работы.
Это директивы из нового стандарта OpenMP 4.0 и из инструментов Intel Cilk Plus.
О директивах нового стандарта OpenMP 4.0 мы поговорим в следующей лекции,
а сейчас рассмотрим инструментарий Intel Cilk Plus.
Директива #pragma simd входит в набор инструментов Cilk Plus.
Она отключает все проверки компилятора о возможности
векторизовать и сообщает компилятору, чтобы тот целиком полагался на то,
что говорит разработчик, и просто векторизует вычисления.
Ответственность в этом случае, естественно,
перекладывается на плечи и голову разработчика.
Если вдруг у вас будет зависимость по данным или еще какие-то условия не
будут выполнены, не позволяющие векторизовать код,
то программа будет работать неправильно, так что действовать нужно аккуратно.
Отсюда и появляется потребность еще в одном способе.
Как бы сделать так, чтобы проверки все же оставались,
но код гарантировано был векторизован?
К сожалению, в существующем C и C++ синтаксисе — пока никак.
А вот с использованием возможности специального синтаксиса для работы с
массивами — RAID Notation, являющегося частью Cilk Plus, это возможно.
Причем синтаксис — весьма простой,
чем-то напоминает Fortran и имеет следующий вид: задаем имя,
начальный индекс, число элементов и шаг (опционально).
Предыдущий пример перепишется с использованием
Cilk Plus следующим образом.
Двоеточие означает, что мы обращаемся ко всем элементам.
Можно также осуществлять более сложные манипуляции.
Скажем, этот код,
где каждому второму элементу массива A присваивается значение элемента массива B,
перепишется более компактно, и главное — гарантирует векторизацию.
Как видите, способов векторизации достаточно много.
Я бы рекомендовал использовать простые способы и максимально
стараться использовать компилятор для создания векторного кода.
В этом случае при выходе нового процессора и новых инструкций вам
потребуется только новый компилятор и перекомпиляция вашей программы,
и вы получаете вашу программу с набором новых инструкций.
А в следующей лекции я расскажу о новых возможностях OpenMP 4.0