Действительно ли эффективен флаг разворота цикла GCC?
В C у меня есть задача, где я должен выполнять умножение, инверсию, преобразование, добавление и т.д. и т.д. с матрицами огромные, выделенными как двумерные массивы (массивы массивов).
Я нашел флаг gcc -funroll-all-loops
. Если я правильно понимаю, это автоматически разворачивает все петли без каких-либо усилий программиста.
Мои вопросы:
a) Включает ли gcc такую оптимизацию с различными флагами оптимизации как -O1
, -O2
и т.д.?
b) Должен ли я использовать любой pragma
внутри моего кода, чтобы воспользоваться возможностью циклирования цикла или автоматически идентифицировать петли?
c) Почему эта опция не включена по умолчанию, если разворот увеличивает производительность?
d) Каковы рекомендуемые флаги оптимизации gcc для компиляции программы наилучшим образом? (Я должен запустить эту программу, оптимизированную для одного семейства процессоров, то же самое на машине, где я компилирую код, на самом деле я использую флаги march=native
и -O2
)
ИЗМЕНИТЬ
Похоже, что существуют разногласия по поводу использования разворота, что в некоторых случаях может замедлить производительность. В моих ситуациях существуют различные методы, которые выполняют только математические операции в 2 вложенных циклах для итерации матричных элементов, выполняемых для огромного количества элементов. В этом случае, как unroll может замедлить или увеличить производительность?
Ответы
Ответ 1
Почему нужно развернуть циклы?
Современные инструкции по конвейеру процессора. Им нравится знать, что будет дальше, и делать всевозможные причудливые оптимизации на основе предположений о том, какой порядок должен выполняться.
В конце цикла, хотя есть две возможности! Либо вы вернетесь к вершине, либо продолжите. Процессор дает обоснованное предположение о том, что произойдет. Если все будет правильно, все будет хорошо. Если нет, он должен очистить конвейер и немного заглохнуть, пока он готовится к работе с другой веткой.
Как вы можете себе представить, разворачивание цикла исключает ветки и потенциал для этих киосков, особенно в тех случаях, когда шансы противоречат предположению.
Представьте себе цикл кода, который выполняется 3 раза, а затем продолжается. Если вы предположите (как, вероятно, процессор), что в конце вы повторите цикл. 2/3 времени, вы будете правы! 1/3 времени, однако, вы остановитесь.
С другой стороны, представьте себе ту же ситуацию, но код цикл трижды. Здесь, возможно, только увеличение 1/3000 времени от разворачивания.
Почему бы не развернуть петли?
Часть упомянутого выше аспекта процессора включает в себя загрузку инструкций из исполняемого файла в память в кеш процессора команд (сокращенный до I-кеша). Это содержит ограниченное количество инструкций, к которым можно получить доступ быстро, но может остановиться, когда новые инструкции необходимо загрузить из памяти.
Вернемся к предыдущим примерам. Предположим, что достаточно малое количество кода внутри цикла занимает n
байты I-кеша. Если мы разворачиваем цикл, он теперь принимает n * 3
байты. Немного больше, но он, вероятно, будет вписываться в одну строку кеша только в порядке, чтобы ваш кеш работал оптимально и не нуждался в чтении из основной памяти.
3000-loop, однако, разворачивается, чтобы использовать колоссальные n * 3000
байты I-кеша. Это потребует нескольких чтений из памяти и, вероятно, вытолкнет некоторые другие полезные материалы из другого места в программе из I-кеша.
Итак, что мне делать?
Как вы можете видеть, разворачивание обеспечивает больше преимуществ для более коротких циклов, но заканчивает работу с ошибкой, если вы собираетесь зацикливать много раз.
Как правило, интеллектуальный компилятор достаточно хорошо разбирается в том, какие циклы будут разворачиваться, но вы можете заставить его, если вы уверены, что знаете лучше. Как вы узнаете лучше? Единственный способ - попробовать в обоих направлениях и сравнить тайминги!
Преждевременная оптимизация - это корень всего зла - Дональд Кнут
Профиль во-первых, оптимизируйте позже.
Ответ 2
Развертка цикла не работает, если компилятор не может предсказать точное количество итераций цикла во время компиляции (или, по крайней мере, предсказать верхнюю границу, а затем пропустить столько итераций, сколько необходимо). Это означает, что если размер вашей матрицы является переменной, флаг не будет иметь эффекта.
Теперь, чтобы ответить на ваши вопросы:
a) Включает ли gcc такую оптимизацию с различными флаги оптимизации как -O1, -O2 и т.д.?
Нет, вам нужно явно установить его, поскольку он может или не может заставить код работать быстрее, и обычно делает исполняемый файл более крупным.
b) Должен ли я использовать какие-либо прагмы внутри моего кода, чтобы воспользоваться возможностью разворачивания цикла или автоматически идентифицировать петли?
Нет прагм. С -funroll-loops
компилятор эвристически решает, какие циклы разворачиваются. Если вы хотите принудительно развернуть, вы можете использовать -funroll-all-loops
, но обычно это замедляет работу кода.
c) Почему эта опция не включена по умолчанию, если разворот увеличивает производительность?
Это не всегда увеличивает производительность! Кроме того, не все идет о производительности. Некоторые люди действительно заботятся о небольших исполняемых файлах, поскольку у них мало памяти (см.: встроенные системы).
d) Каковы рекомендуемые флаги оптимизации gcc для компиляции программы наилучшим образом? (Я должен запустить эту программу, оптимизированную для одного семейства процессоров, то же самое на машине, где я компилирую код, на самом деле я использую флаги march = native и -O2)
Нет серебряной пули. Вам нужно будет подумать, проверить и посмотреть. На самом деле существует теорема о том, что идеальный компилятор вообще не существует.
Вы прокомментировали свою программу? Профилирование - очень полезный навык для этих вещей.
Источник (в основном): https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html
Ответ 3
Вы получаете теоретический фон об этой проблеме, и это оставляет достаточно места, чтобы угадать, что вы получаете в реальном прогоне. Говорят, что этот вариант не всегда увеличивает производительность, поскольку он зависит от множества факторов, например, от реализации цикла, его загрузки/тела и других.
Каждый код отличается, и если вы заинтересованы в поиске лучшего решения производительности, то неплохо запустить оба варианта, измерить время их выполнения и сравнить.
Посмотрите на этот подход в ответе ниже, чтобы иметь представление о измерении времени. В двух словах вы просто сворачиваете свой код в цикл, который приведет вашу программу к выполнению нескольких секунд. Поскольку вы сами оптимизируете петли, неплохо написать оболочку script, которая запускает ваше приложение много раз.