Целью этой статьи является освещение возможностей оптимизации компиляторов с упором на компиляторы Intel C++ , известные своей популярностью и широким распространением.
Основные моменты: Что такое оптимизация компилятора? | -Вкл | Целевая архитектура | Межпроцедурная оптимизация | -fno-алиасинг | Отчеты об оптимизации компилятора
Любой компилятор выполняет ряд шагов для преобразования исходного кода высокого уровня в машинный код низкого уровня. Они включают лексический анализ, синтаксический анализ, семантический анализ, генерацию промежуточного кода (или IR), оптимизацию и генерацию кода.
На этапе оптимизации компилятор тщательно ищет способы преобразования программы, стремясь к семантически эквивалентному результату, который использует меньше ресурсов или выполняется быстрее. Методы, используемые в этом процессе, включают, помимо прочего , свертывание констант, оптимизацию цикла, встраивание функций и устранение мертвого кода .
Разработчики могут указать набор флагов компилятора во время процесса компиляции — практика, знакомая тем, кто использует такие параметры, как « -g» или «-pg» с GCC для отладки и профилирования информации. По ходу дела мы обсудим аналогичные флаги компилятора, которые можно использовать при компиляции нашего приложения с помощью компилятора Intel C++. Это может помочь вам повысить эффективность и производительность вашего кода.
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]; } } }
MFLOP/s означает «миллион операций с плавающей запятой в секунду». Это единица измерения, используемая для количественной оценки производительности компьютера или процессора с точки зрения операций с плавающей запятой. Операции с плавающей запятой включают математические вычисления с десятичными или действительными числами, представленными в формате с плавающей запятой.
Примечание 1. Чтобы обеспечить стабильный результат, я запускаю исполняемый файл 5 раз для каждого разрешения и беру среднее значение значений MFLOP/s.
Примечание 2. Важно отметить, что оптимизацией по умолчанию для компилятора Intel C++ является -O2. Поэтому важно указать -O0 при компиляции исходного кода.
Это некоторые из наиболее часто используемых флагов компилятора, когда кто-то начинает оптимизировать компилятор. В идеальном случае производительность Ofast > O3 > O2 > O1 > O0 . Однако это не обязательно происходит. Критические моменты этих вариантов заключаются в следующем:
-O1:
-О2:
-O3:
-Офаст:
Совершенно очевидно, что все эти оптимизации выполняются намного быстрее, чем наш базовый код (с «-O0»). Время выполнения в 2–3 раза меньше, чем в базовом варианте. А как насчет MFLOP/s??
В целом, хотя и незначительно, «-O3» работает лучше всего.
Дополнительные флаги, используемые « -Ofast » (« -no-prec-div -fp-model fast=2 »), не дают никакого дополнительного ускорения.
Ответ кроется в стратегических флагах компилятора. Экспериментирование с такими опциями, как « -xHost » и, точнее, « -xCORE-AVX512 », может позволить нам использовать весь потенциал возможностей машины и адаптировать оптимизацию для оптимальной производительности.
-xХост:
-xCORE-AVX512:
Цель: явно указать компилятору сгенерировать код, использующий набор инструкций Intel Advanced Vector Extensions 512 (AVX-512).
Ключевые особенности: AVX-512 — это расширенный набор инструкций SIMD (одна инструкция, несколько данных), который предлагает более широкие векторные регистры и дополнительные операции по сравнению с предыдущими версиями, такими как AVX2. Включение этого флага позволяет компилятору использовать эти расширенные функции для оптимизации производительности.
Соображения: здесь снова виновата портативность. Двоичные файлы, созданные с помощью инструкций AVX-512, могут работать неоптимально на процессорах, не поддерживающих этот набор инструкций. Они могут вообще не работать!
По умолчанию « -xCORE-AVX512 » предполагает, что программа вряд ли выиграет от использования регистров zmm. Компилятор избегает использования регистров zmm, если не гарантирован прирост производительности.
Если вы планируете использовать регистры zmm без ограничений, для « » можно установить высокое значение. Это то, что мы тоже будем делать.
Уууу!
Примечательно то, что мы достигли этих результатов без какого-либо существенного ручного вмешательства — просто за счет включения нескольких флагов компилятора в процесс компиляции приложения.
Примечание. Не волнуйтесь, если ваше оборудование не поддерживает AVX-512. Компилятор Intel C++ поддерживает оптимизацию для AVX, AVX-2 и даже SSE. В есть все, что вам нужно знать!
IPO — это многоэтапный процесс, ориентированный на взаимодействие между различными функциями или процедурами внутри программы. IPO может включать в себя множество различных видов оптимизации, включая прямую замену, косвенное преобразование вызовов и встраивание.
-ипо:
Цель: обеспечивает межпроцедурную оптимизацию, позволяя компилятору анализировать и оптимизировать всю программу, за исключением отдельных исходных файлов, во время компиляции.
Ключевые особенности: - Оптимизация всей программы: « -ipo » выполняет анализ и оптимизацию всех исходных файлов, учитывая взаимодействие между функциями и процедурами во всей программе. - Межфункциональная и межмодульная оптимизация: флаг облегчает встраивание функций, синхронизацию. оптимизаций и анализа потоков данных в различных частях программы.
Соображения: Требуется отдельный шаг связывания. После компиляции с использованием « -ipo » необходим определенный шаг компоновки для создания окончательного исполняемого файла. Во время компоновки компилятор выполняет дополнительную оптимизацию на основе всего представления программы.
-IP:
Цель: обеспечивает межпроцедурный анализ-распространение, позволяя компилятору выполнять некоторые межпроцедурные оптимизации без необходимости отдельного этапа связывания.
Ключевые особенности: - Анализ и распространение: « -ip » позволяет компилятору выполнять исследование и распространение данных между различными функциями и модулями во время компиляции. Однако он не выполняет все оптимизации, требующие полного просмотра программы. Ускоренная компиляция: в отличие от « -ipo », « -ip » не требует отдельного этапа компоновки, что приводит к ускорению компиляции. Это может быть полезно во время разработки, когда важна быстрая обратная связь.
Соображения: Происходят лишь некоторые ограниченные межпроцедурные оптимизации, включая встраивание функций.
-ipo обычно предоставляет более широкие возможности межпроцедурной оптимизации, поскольку включает отдельный этап компоновки, но за это приходится платить более длительным временем компиляции. [ ] -ip — более быстрая альтернатива, которая выполняет некоторые межпроцедурные оптимизации, не требуя отдельного этапа связывания, что делает ее подходящей для этапов разработки и тестирования.[ ]
Поскольку мы говорим только о производительности и различных оптимизациях, времени компиляции или размере исполняемого файла, нас это не касается, мы сосредоточимся на « -ipo ».
/* * 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. Когда любой компилятор видит эту функцию в исходном файле, ему следует быть очень осторожным.
Выражение для вычисления new с использованием u включает в себя среднее значение 4 соседних значений u . Что, если и u , и unew указывают на одно и то же место? Это станет классической проблемой псевдонимов указателей [ ].
Современные компиляторы очень умны и для обеспечения безопасности предполагают, что псевдонимы возможны. И в подобных сценариях они избегают любых оптимизаций, которые могут повлиять на семантику и выходные данные кода.
В нашем случае мы знаем, что u и unew — это разные области памяти и предназначены для хранения разных значений. Итак, мы можем легко сообщить компилятору, что здесь не будет никаких псевдонимов.
Есть два метода. Во-первых, это ключевое слово C « » . Но это требует изменения кода. Мы пока этого не хотим.
Что-нибудь простое? Давайте попробуем « -fno-alias ».
-fno-псевдоним:
Цель: дать указание компилятору не допускать псевдонимов в программе.
Ключевые особенности: При отсутствии псевдонимов компилятор может более свободно оптимизировать код, потенциально повышая производительность.
Соображения: разработчик должен быть осторожным при использовании этого флага, так как в случае любого необоснованного псевдонима программа может выдать неожиданные выходные данные.
Ну, теперь у нас есть что-то!!!
Более внимательное изучение ассемблерного кода (хотя и не представленного здесь) и сгенерированного отчета об оптимизации компиляции (см . ниже ) показывает, что компилятор умело применяет и . Эти преобразования способствуют высокой оптимизации производительности, демонстрируя значительное влияние директив компилятора на эффективность кода.
Компилятор Intel C++ предоставляет ценную функцию, которая позволяет пользователям создавать отчет об оптимизации, суммирующий все корректировки, внесенные в целях оптимизации [ ]. Этот подробный отчет сохраняется в формате файла YAML и представляет подробный список оптимизаций, примененных компилятором в коде. Подробное описание смотрите в официальной документации по « ».
Точно так же компиляторы Intel C++ (и все популярные) также поддерживают директивы pragma, что является очень полезной функцией. Стоит проверить некоторые прагмы, такие как ivdep, Parallel, Simd, Vector и т. д., в .