Достижение максимальной производительности вашего кода C++ может оказаться непростой задачей, требующей тщательного профилирования, сложной настройки доступа к памяти и оптимизации кэша. Есть ли способ немного упростить это?? К счастью, есть кратчайший путь к значительному увеличению производительности с минимальными усилиями — при условии, что у вас есть правильное понимание и вы знаете, что делаете. Введите оптимизации компилятора, которые могут значительно повысить производительность вашего кода. Современные компиляторы служат незаменимыми союзниками на пути к оптимальной производительности, особенно при автоматическом распараллеливании. Эти сложные инструменты способны тщательно изучать сложные шаблоны кода, особенно внутри циклов, и беспрепятственно выполнять оптимизацию. Целью этой статьи является освещение возможностей оптимизации компиляторов с упором на , известные своей популярностью и широким распространением. компиляторы Intel C++ В этой истории мы раскрываем слои магии компилятора, которые могут превратить ваш код в высокопроизводительный шедевр, требующий меньше ручного вмешательства, чем вы думаете. Что такое оптимизация компилятора? | -Вкл | Целевая архитектура | Межпроцедурная оптимизация | -fno-алиасинг | Отчеты об оптимизации компилятора Основные моменты: Что такое оптимизация компилятора? Оптимизация компилятора включает в себя различные методы и преобразования, которые компилятор применяет к исходному коду во время компиляции. Но почему? Для повышения производительности, эффективности и, в некоторых случаях, размера результирующего машинного кода. Эти оптимизации имеют решающее значение для влияния на различные аспекты выполнения кода, включая скорость, использование памяти и энергопотребление. Любой компилятор выполняет ряд шагов для преобразования исходного кода высокого уровня в машинный код низкого уровня. Они включают лексический анализ, синтаксический анализ, семантический анализ, генерацию промежуточного кода (или IR), оптимизацию и генерацию кода. На этапе оптимизации компилятор тщательно ищет способы преобразования программы, стремясь к семантически эквивалентному результату, который использует меньше ресурсов или выполняется быстрее. Методы, используемые в этом процессе, включают, помимо прочего . , свертывание констант, оптимизацию цикла, встраивание функций и устранение мертвого кода Я не собираюсь обсуждать все доступные варианты, а расскажу о том, как мы можем поручить компилятору выполнить конкретную оптимизацию, которая может улучшить производительность кода. Итак, решение???? Флаги компилятора. Разработчики могут указать набор флагов компилятора во время процесса компиляции — практика, знакомая тем, кто использует такие параметры, как « или с GCC для отладки и профилирования информации. По ходу дела мы обсудим аналогичные флаги компилятора, которые можно использовать при компиляции нашего приложения с помощью компилятора Intel C++. Это может помочь вам повысить эффективность и производительность вашего кода. -g» «-pg» Итак, с чем мы работаем? Я не буду углубляться в сухую теорию или загружать вас утомительной документацией, в которой перечислены все флаги компилятора. Вместо этого давайте попробуем понять, почему и как работают эти флаги. Как нам этого добиться??? Мы возьмем неоптимизированную функцию C++, отвечающую за вычисление итерации , и шаг за шагом выясним влияние каждого флага компилятора. В ходе этого исследования мы будем измерять ускорение, систематически сравнивая каждую итерацию с базовой версией — начиная с отсутствия флагов оптимизации (-O0). Якоби Ускорение (или время выполнения) измерялось на компьютере . Здесь метод Якоби решает двумерное уравнение в частных производных (уравнение Пуассона) для моделирования распределения тепла на прямоугольной сетке. с процессором Intel® Xeon® Platinum 8174 u(x,y,t) — температура в точке (x,y) в момент времени t. Мы решаем стабильное состояние, когда распределение больше не меняется: На границе применен набор граничных условий Дирихле. По сути, у нас есть код C++, выполняющий итерации Якоби на сетках переменных размеров (которые мы называем разрешениями). По сути, размер сетки 500 означает решение матрицы размером 500x500 и так далее. Функция для выполнения одной итерации Якоби выглядит следующим образом: /* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } } Мы продолжаем выполнять итерацию Якоби до тех пор, пока остаток не достигнет порогового значения (внутри цикла). Вычисление остатка и оценка порога выполняются вне этой функции и здесь не имеют значения. Итак, давайте теперь поговорим о слоне в комнате! Как работает базовый код? Без оптимизации (-O0) мы получаем следующие результаты: Здесь мы измеряем производительность в MFLOP/s. Это будет основой нашего сравнения. MFLOP/s означает «миллион операций с плавающей запятой в секунду». Это единица измерения, используемая для количественной оценки производительности компьютера или процессора с точки зрения операций с плавающей запятой. Операции с плавающей запятой включают математические вычисления с десятичными или действительными числами, представленными в формате с плавающей запятой. MFLOP/s часто используется в качестве эталона или показателя производительности, особенно в научных и инженерных приложениях, где преобладают сложные математические вычисления. Чем выше значение MFLOP/s, тем быстрее система или процессор выполняет операции с плавающей запятой. Чтобы обеспечить стабильный результат, я запускаю исполняемый файл 5 раз для каждого разрешения и беру среднее значение значений MFLOP/s. Примечание 1. Важно отметить, что оптимизацией по умолчанию для компилятора Intel C++ является -O2. Поэтому важно указать -O0 при компиляции исходного кода. Примечание 2. Давайте продолжим и посмотрим, как будет меняться время выполнения при использовании разных флагов компилятора! Наиболее распространенные: -O1, -O2, -O3 и -Ofast. Это некоторые из наиболее часто используемых флагов компилятора, когда кто-то начинает оптимизировать компилятор. В идеальном случае производительность . Однако это не обязательно происходит. Критические моменты этих вариантов заключаются в следующем: Ofast > O3 > O2 > O1 > O0 -O1: оптимизировать скорость, избегая увеличения размера кода. Цель: Подходит для приложений с большими размерами кода, множеством ветвей и где время выполнения не зависит от кода внутри циклов. Ключевые особенности: -О2: Улучшения по сравнению с -O1: Включает векторизацию. Позволяет встраивать встроенные функции и внутрифайловую межпроцедурную оптимизацию. -O3: Улучшения по сравнению с -O2: Позволяет более агрессивные преобразования циклов (Fusion, Block-Unroll-and-Jam). Оптимизации могут постоянно превосходить -O2 только в том случае, если происходят преобразования циклов и доступа к памяти. Это может даже замедлить работу кода. Рекомендуется для: Приложения с тяжелыми циклами вычислений с плавающей запятой и большими наборами данных. -Офаст: Устанавливает следующие флаги: «-О3» «- : позволяет выполнять оптимизацию, которая дает быстрые и немного менее точные результаты, чем полное деление IEEE. Например, A/B вычисляется как A * (1/B) для повышения скорости вычислений. no-prec-div» « : включает более агрессивную оптимизацию с плавающей запятой. -fp-model fast=2» В подробно рассказывается о том, какие именно оптимизации предлагают эти опции. официальном руководстве При использовании этих параметров в нашем коде Jacobi мы получаем следующее время выполнения: Совершенно очевидно, что все эти оптимизации выполняются намного быстрее, чем наш базовый код (с «-O0»). Время выполнения в 2–3 раза меньше, чем в базовом варианте. А как насчет MFLOP/s?? Ну это что-то!!! Существует большая разница между MFLOP/s в базовом варианте и в случае оптимизации. В целом, хотя и незначительно, «-O3» работает лучше всего. Дополнительные флаги, используемые « » (« »), не дают никакого дополнительного ускорения. -Ofast -no-prec-div -fp-model fast=2 Целевая архитектура (-xHost,-xCORE-AVX512) Архитектура машины является ключевым фактором, влияющим на оптимизацию компилятора. Это может значительно повысить производительность, если компилятор знает доступные наборы команд и оптимизации, поддерживаемые оборудованием (например, векторизацию и SIMD). Например, моя машина Skylake имеет 3 блока SIMD: 1 AVX 512 и 2 блока AVX-2. Могу ли я реально что-то сделать с этими знаниями??? Ответ кроется в стратегических флагах компилятора. Экспериментирование с такими опциями, как « » и, точнее, « », может позволить нам использовать весь потенциал возможностей машины и адаптировать оптимизацию для оптимальной производительности. -xHost -xCORE-AVX512 Вот краткое описание того, что означают эти флаги: -xХост: указывает, что компилятор должен генерировать код, оптимизированный для самого высокого набора команд хост-машины. Цель: Использует новейшие функции и возможности, доступные на оборудовании. Это может дать потрясающее ускорение целевой системы. Ключевые особенности: Хотя этот флаг оптимизирует архитектуру хоста, это может привести к тому, что двоичные файлы не будут переноситься на разные машины с разными архитектурами набора команд. Соображения: -xCORE-AVX512: явно указать компилятору сгенерировать код, использующий набор инструкций Intel Advanced Vector Extensions 512 (AVX-512). Цель: AVX-512 — это расширенный набор инструкций SIMD (одна инструкция, несколько данных), который предлагает более широкие векторные регистры и дополнительные операции по сравнению с предыдущими версиями, такими как AVX2. Включение этого флага позволяет компилятору использовать эти расширенные функции для оптимизации производительности. Ключевые особенности: здесь снова виновата портативность. Двоичные файлы, созданные с помощью инструкций AVX-512, могут работать неоптимально на процессорах, не поддерживающих этот набор инструкций. Они могут вообще не работать! Соображения: В инструкциях установки AVX-512 используются регистры Zmm, которые представляют собой набор регистров шириной 512 бит. Эти регистры служат основой для векторной обработки. По умолчанию « » предполагает, что программа вряд ли выиграет от использования регистров zmm. Компилятор избегает использования регистров zmm, если не гарантирован прирост производительности. -xCORE-AVX512 Если вы планируете использовать регистры zmm без ограничений, для « » можно установить высокое значение. Это то, что мы тоже будем делать. -qopt-zmm-usage Не забудьте проверить для получения подробных инструкций. официальное руководство Давайте посмотрим, как эти флаги работают в нашем коде: Уууу! Теперь мы пересекаем отметку 1200 MFLOP/s для наименьшего разрешения. Значения MFLOP/s для других разрешений также увеличились. Примечательно то, что мы достигли этих результатов без какого-либо существенного ручного вмешательства — просто за счет включения нескольких флагов компилятора в процесс компиляции приложения. Однако важно подчеркнуть, что скомпилированный исполняемый файл будет совместим только с машиной, использующей тот же набор команд. Компромисс между оптимизацией и переносимостью очевиден, поскольку код, оптимизированный для определенного набора команд, может принести в жертву переносимость между различными конфигурациями оборудования. Итак, убедитесь, что вы знаете, что делаете! Не волнуйтесь, если ваше оборудование не поддерживает AVX-512. Компилятор Intel C++ поддерживает оптимизацию для AVX, AVX-2 и даже SSE. В есть все, что вам нужно знать! Примечание. документации Межпроцедурная оптимизация (IPO) Межпроцедурная оптимизация включает в себя анализ и преобразование кода для нескольких функций или процедур, выходя за рамки отдельных функций. IPO — это многоэтапный процесс, ориентированный на взаимодействие между различными функциями или процедурами внутри программы. IPO может включать в себя множество различных видов оптимизации, включая прямую замену, косвенное преобразование вызовов и встраивание. Intel Compiler поддерживает два распространенных типа IPO: компиляцию одного файла и компиляцию нескольких файлов (оптимизация всей программы) [ ]. Существует два общих флага компилятора, выполняющих каждый из них: 3 -ипо: обеспечивает межпроцедурную оптимизацию, позволяя компилятору анализировать и оптимизировать всю программу, за исключением отдельных исходных файлов, во время компиляции. Цель: Оптимизация всей программы: « » выполняет анализ и оптимизацию всех исходных файлов, учитывая взаимодействие между функциями и процедурами во всей программе. - Межфункциональная и межмодульная оптимизация: флаг облегчает встраивание функций, синхронизацию. оптимизаций и анализа потоков данных в различных частях программы. Ключевые особенности: - -ipo Требуется отдельный шаг связывания. После компиляции с использованием « » необходим определенный шаг компоновки для создания окончательного исполняемого файла. Во время компоновки компилятор выполняет дополнительную оптимизацию на основе всего представления программы. Соображения: -ipo -IP: обеспечивает межпроцедурный анализ-распространение, позволяя компилятору выполнять некоторые межпроцедурные оптимизации без необходимости отдельного этапа связывания. Цель: Анализ и распространение: « » позволяет компилятору выполнять исследование и распространение данных между различными функциями и модулями во время компиляции. Однако он не выполняет все оптимизации, требующие полного просмотра программы. Ускоренная компиляция: в отличие от « », « » не требует отдельного этапа компоновки, что приводит к ускорению компиляции. Это может быть полезно во время разработки, когда важна быстрая обратная связь. Ключевые особенности: - -ip -ipo -ip Происходят лишь некоторые ограниченные межпроцедурные оптимизации, включая встраивание функций. Соображения: -ipo обычно предоставляет более широкие возможности межпроцедурной оптимизации, поскольку включает отдельный этап компоновки, но за это приходится платить более длительным временем компиляции. [ ] 4 -ip — более быстрая альтернатива, которая выполняет некоторые межпроцедурные оптимизации, не требуя отдельного этапа связывания, что делает ее подходящей для этапов разработки и тестирования.[ ] 5 Поскольку мы говорим только о производительности и различных оптимизациях, времени компиляции или размере исполняемого файла, нас это не касается, мы сосредоточимся на « ». -ipo -fno-псевдоним Все вышеперечисленные оптимизации зависят от того, насколько хорошо вы знаете свое оборудование и насколько много вы готовы экспериментировать. Но это не все. Если мы попытаемся определить, как компилятор будет видеть наш код, мы сможем выявить другие потенциальные варианты оптимизации. Давайте еще раз посмотрим на наш код: /* * One Jacobi iteration step */ void jacobi(double *u, double *unew, unsigned sizex, unsigned sizey) { int i, j; for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { unew[i * sizex + j] = 0.25 * (u[i * sizex + (j - 1)] + // left u[i * sizex + (j + 1)] + // right u[(i - 1) * sizex + j] + // top u[(i + 1) * sizex + j]); // bottom } } for (j = 1; j < sizex - 1; j++) { for (i = 1; i < sizey - 1; i++) { u[i * sizex + j] = unew[i * sizex + j]; } } } Функция jacobi() принимает пару указателей в качестве параметров, а затем выполняет что-то внутри вложенных циклов for. Когда любой компилятор видит эту функцию в исходном файле, ему следует быть очень осторожным. Почему?? Выражение для вычисления с использованием включает в себя среднее значение 4 соседних значений . Что, если и , и указывают на одно и то же место? Это станет классической проблемой [ ]. new u u u unew псевдонимов указателей 7 Современные компиляторы очень умны и для обеспечения безопасности предполагают, что псевдонимы возможны. И в подобных сценариях они избегают любых оптимизаций, которые могут повлиять на семантику и выходные данные кода. В нашем случае мы знаем, что и — это разные области памяти и предназначены для хранения разных значений. Итак, мы можем легко сообщить компилятору, что здесь не будет никаких псевдонимов. u unew Как мы это делаем? Есть два метода. Во-первых, это . Но это требует изменения кода. Мы пока этого не хотим. ключевое слово C « » limit Что-нибудь простое? Давайте попробуем « ». -fno-alias -fno-псевдоним: дать указание компилятору не допускать псевдонимов в программе. Цель: При отсутствии псевдонимов компилятор может более свободно оптимизировать код, потенциально повышая производительность. Ключевые особенности: разработчик должен быть осторожным при использовании этого флага, так как в случае любого необоснованного псевдонима программа может выдать неожиданные выходные данные. Соображения: Более подробную информацию можно найти в . официальной документации Как это отразится на нашем коде? Ну, теперь у нас есть что-то!!! Здесь мы добились значительного ускорения, почти в 3 раза по сравнению с предыдущими оптимизациями. В чем секрет этого повышения? Поручив компилятору не допускать псевдонимов, мы дали ему свободу использовать мощные оптимизации циклов. Более внимательное изучение ассемблерного кода (хотя и не представленного здесь) и сгенерированного отчета об оптимизации компиляции (см ) показывает, что компилятор умело применяет и . Эти преобразования способствуют высокой оптимизации производительности, демонстрируя значительное влияние директив компилятора на эффективность кода. . ниже обмен циклами развертывание циклов Итоговые графики Вот как все оптимизации работают друг против друга: Отчет об оптимизации компилятора (-qopt-report) Компилятор Intel C++ предоставляет ценную функцию, которая позволяет пользователям создавать отчет об оптимизации, суммирующий все корректировки, внесенные в целях оптимизации [ ]. Этот подробный отчет сохраняется в формате файла YAML и представляет подробный список оптимизаций, примененных компилятором в коде. Подробное описание смотрите в официальной документации по « ». 8 -qopt-report Что дальше? Мы обсудили несколько флагов компилятора, которые могут значительно улучшить производительность нашего кода без особых усилий с нашей стороны. Единственное условие: ничего не делайте вслепую; убедитесь, что вы знаете, что делаете! Таких флагов компилятора существуют сотни, и в этой истории рассказывается о нескольких из них. Итак, стоит просмотреть официальное руководство по компилятору вашего любимого компилятора (особенно документацию, связанную с оптимизацией). Помимо этих флагов компилятора, существует целый ряд методов, таких как векторизация, встроенные функции SIMD, и , которые могут значительно улучшить производительность вашего кода. оптимизация по профилю управляемый автоматический параллелизм Точно так же компиляторы Intel C++ (и все популярные) также поддерживают директивы pragma, что является очень полезной функцией. Стоит проверить некоторые прагмы, такие как и т. д., в . ivdep, Parallel, Simd, Vector Intel-Specific Pragma Reference Рекомендуемое чтение [1] Оптимизация и программирование (intel.com) [2] Высокопроизводительные вычисления с «Эльветричем» в Университете Кайзерслаутерн-Ландау (rptu.de) [3] Межпроцедурная оптимизация (intel.com) [4] IPO, Qipo (intel.com) [5] ip, Qip (intel.com) [6] Флаги компилятора Intel, оптимизации и другие флаги для использования SPEChpc. [7] Псевдонимы — Документация IBM [8] Отчеты об оптимизации компилятора Intel® Рекомендуемое фото на . Игоря Омилаева Unsplash Также опубликовано . здесь