Опасен ли уровень оптимизации -O3 в g ++?

233

Я слышал из разных источников (хотя в основном от моего коллеги), что компиляция с уровнем оптимизации -O3в g ++ как-то «опасна», и ее следует избегать в целом, если в этом нет необходимости.

Это правда, и если да, то почему? Должен ли я просто придерживаться -O2?

Dunnie
источник
38
Это опасно, только если вы полагаетесь на неопределенное поведение. И даже тогда я был бы удивлен, если это был уровень оптимизации, который что-то испортил.
Сет Карнеги
5
Компилятор все еще вынужден создавать программу, которая ведет себя «как будто», он точно скомпилировал ваш код. Я не знаю, что -O3считается особенно глючит? Я думаю, что, возможно, это может сделать неопределенное поведение «хуже», поскольку оно может делать странные и удивительные вещи, основанные на определенных предположениях, но это будет вашей собственной ошибкой. В общем, я бы сказал, что все в порядке.
BoBTFish
5
Это правда, что более высокие уровни оптимизации более подвержены ошибкам компилятора. Я сам поразил несколько случаев, но в целом они все еще довольно редки.
Мистик
21
-O2включается -fstrict-aliasing, и если ваш код выживет, то он, вероятно, переживет другие оптимизации, поскольку люди ошибаются снова и снова. Тем не менее, -fpredictive-commoningэто только в -O3, и включение, которое может привести к ошибкам в вашем коде, вызванным неправильными предположениями о параллелизме. Чем менее неправильный ваш код, тем менее опасна оптимизация ;-)
Стив Джессоп
6
@PlasmaHH, я не думаю, что "более строгое" - хорошее описание -Ofast, оно отключает, например, IEEE-совместимую обработку NaN
Джонатан Уэйкли,

Ответы:

223

В первые дни gcc (2.8 и т. Д.), А также во времена egcs и redhat 2.96-O3 иногда были довольно глючными. Но это более десяти лет назад, и -O3 мало чем отличается от других уровней оптимизации (в том, что касается ошибок).

Тем не менее, он, как правило, выявляет случаи, когда люди полагаются на неопределенное поведение, поскольку они более строго следуют правилам и особенно угловым случаям языка (ов).

Что касается меня, я уже много лет работаю с производственным программным обеспечением в финансовом секторе с -O3 и еще не столкнулся с ошибкой, которой бы не было, если бы я использовал -O2.

По многочисленным просьбам вот дополнение:

-O3 и особенно дополнительные флаги, такие как -funroll-loops (не включены -O3), могут иногда приводить к генерированию большего количества машинного кода. При определенных обстоятельствах (например, на процессоре с исключительно небольшим кешем команд L1) это может вызвать замедление из-за всего кода, например, некоторого внутреннего цикла, который больше не вписывается в L1I. Обычно gcc старается не генерировать так много кода, но так как он обычно оптимизирует общий случай, это может произойти. Опции, особенно склонные к этому (например, развертывание цикла), обычно не включаются в -O3 и соответственно отмечаются на странице руководства. Поэтому обычно рекомендуется использовать -O3 для генерации быстрого кода и использовать только -O2 или -Os (который пытается оптимизировать размер кода), когда это уместно (например, когда профилировщик указывает, что L1I отсутствует).

Если вы хотите довести оптимизацию до крайности, вы можете настроить gcc с помощью --param затрат, связанных с определенной оптимизацией. Кроме того, обратите внимание, что теперь у gcc есть возможность помещать атрибуты в функции, которые управляют настройками оптимизации только для этих функций, поэтому, когда вы обнаружите, что у вас есть проблема с -O3 в одной функции (или хотите попробовать специальные флаги только для этой функции), Вам не нужно компилировать весь файл или даже весь проект с O2.

Ото кажется, что следует соблюдать осторожность при использовании -Ofast, который гласит:

-Ofast включает все оптимизации -O3. Это также позволяет оптимизировать не все стандартные программы.

что заставляет меня заключить, что -O3 предназначен для полного соответствия стандартам.

PlasmaHH
источник
2
Я просто использую что-то вроде обратного. Я всегда использую -Os или -O2 (иногда O2 генерирует исполняемый файл меньшего размера). После профилирования я использую O3 в тех частях кода, которые занимают больше времени выполнения и которые сами по себе могут увеличить скорость до 20%.
CoffeDeveloper
3
Я делаю это для скорости. O3 в большинстве случаев делает вещи медленнее. Не знаю точно, почему, я подозреваю, это загрязняет кэш инструкций.
CoffeDeveloper
4
@DarioOO Я чувствую, что умолять "раздувание кода" является популярным делом, но я почти никогда не вижу его подкрепленным тестами. Это во многом зависит от архитектуры, но каждый раз, когда я вижу опубликованные тесты (например, phoronix.com/… ), он показывает, что O3 быстрее в подавляющем большинстве случаев. Я видел профилирование и тщательный анализ, необходимый для того, чтобы доказать, что раздувание кода на самом деле является проблемой, и обычно это случается только с людьми, которые чрезвычайно воспринимают шаблоны.
Нир Фридман
1
@NirFriedman: Это имеет тенденцию вызывать проблемы, когда в модели затрат компилятора со встроенными компонентами есть ошибки или когда вы оптимизируете для цели, совершенно отличной от той, на которой вы работаете. Интересно, что это относится ко всем уровням оптимизации ...
PlasmaHH
1
@PlasmaHH: проблема использования cmov будет трудно исправить в общем случае. Обычно вы не просто сортируете свои данные, поэтому, когда gcc пытается решить, является ли ветвь предсказуемой или нет, статический анализ поиска вызовов std::sortфункций вряд ли поможет. Было бы полезно использовать что-то вроде stackoverflow.com/questions/109710/… или, возможно, написать исходный код, чтобы воспользоваться преимуществами сортировки: сканирование до тех пор, пока вы не увидите> = 128, затем начните суммирование. Что касается раздутого кода, то я собираюсь сообщить об этом. : P
Питер Кордес
42

По моему довольно скучному опыту, применение -O3ко всей программе почти всегда делает ее медленнее (по сравнению с -O2), потому что включает агрессивное развертывание цикла и встраивание, что делает программу больше не вписывающейся в кэш команд. Для более крупных программ это также может быть верно для -O2относительно -Os!

Предполагаемый шаблон использования для -O3: после профилирования вашей программы вы вручную применяете ее к небольшому числу файлов, содержащих критические внутренние циклы, которые на самом деле выигрывают от этих агрессивных компромиссов между скоростью и пространством. Более новые версии GCC имеют режим оптимизации по профилю, который может (IIUC) выборочно применять -O3оптимизации к горячим функциям - эффективно автоматизируя этот процесс.

zwol
источник
10
"почти всегда"? Сделайте это "50-50", и мы договоримся ;-).
No-Bugs Hare
12

Опция -O3 включает более дорогие оптимизации, такие как встраивание функций, в дополнение ко всем оптимизациям нижних уровней '-O2' и '-O1'. Уровень оптимизации '-O3' может увеличить скорость результирующего исполняемого файла, но также может увеличить его размер. При некоторых обстоятельствах, когда эти оптимизации не являются благоприятными, эта опция может на самом деле замедлить работу программы.

Ниль
источник
3
Я понимаю, что некоторые "очевидные оптимизации" могут замедлить выполнение программы, но есть ли у вас источник, который утверждает, что GCC-O3 замедлил программу?
Mooing Duck
1
@MooingDuck: хотя я не могу ссылаться на источник, я помню, что сталкивался с таким случаем с некоторыми более старыми процессорами AMD, которые имели довольно маленький кэш L1I (~ 10 тыс. Инструкций). Я уверен, что у Google есть больше для заинтересованных, но особенно такие опции, как развертывание цикла, не являются частью O3, и они значительно увеличивают размеры. -Ос тот, когда вы хотите сделать исполняемый файл наименьшим. Даже -O2 может увеличить размер кода. Хорошим инструментом для работы с результатами различных уровней оптимизации является gcc explorer.
PlasmaHH
@PlasmaHH: На самом деле крошечный размер кэша - это то, что компилятор может испортить, хорошая мысль. Это действительно хороший пример. Пожалуйста, поместите это в ответ.
Мычанка
1
@PlasmaHH Pentium III имел 16 КБ кеш-кода. AMD K6 и выше на самом деле имел кеш инструкций 32KB. P4 начал с около 96 КБ. Core I7 на самом деле имеет кеш-код L1 32 КБ. В наше время сильные декодеры команд, так что ваш L3 достаточно хорош, чтобы использовать его практически для любого цикла.
doug65536
1
Вы увидите огромное увеличение производительности в любое время, когда в цикле есть функция, вызываемая в цикле, и она может выполнять значительное общее удаление подвыражений и выводить ненужные пересчеты из функции перед циклом.
doug65536
8

Да, O3 хуже. Я разработчик компилятора, и я обнаружил явные и очевидные ошибки gcc, вызванные тем, что O3 генерировал ошибочные инструкции по сборке SIMD при сборке собственного программного обеспечения. Из того, что я видел, большинство производственного программного обеспечения поставляется с O2, что означает, что O3 будет уделяться меньше внимания при тестировании и исправлении ошибок.

Подумайте об этом так: O3 добавляет больше преобразований поверх O2, что добавляет больше преобразований поверх O1. По статистике, больше трансформаций означает больше ошибок. Это верно для любого компилятора.

Дэвид Йегер
источник
3

Недавно у меня возникла проблема с использованием оптимизации с g++. Проблема была связана с картой PCI, где регистры (для команд и данных) были переэкрантированы по адресу памяти. Мой драйвер сопоставил физический адрес с указателем в приложении и передал его вызываемому процессу, который работал с ним так:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

Карта не работает, как ожидалось. Когда я увидел сборку я понял , что компилятор написал только someCommand[ the last ]в pciMemory, минуя все предыдущие записи.

В заключение: будьте аккуратны и внимательны с оптимизацией.

borisbn
источник
38
Но дело в том, что ваша программа просто имеет неопределенное поведение; оптимизатор не сделал ничего плохого. В частности, вы должны объявить pciMemoryкак volatile.
Конрад Рудольф
11
На самом деле это не UB, но компилятор имеет право опустить все записи, кроме последней, pciMemoryпотому что все другие записи доказуемо не имеют никакого эффекта. Для оптимизатора это удивительно, потому что он может удалить много бесполезных и отнимающих много времени инструкций.
Конрад Рудольф
4
Я нашел это в стандарте (после 10+ лет))) - энергозависимое объявление может использоваться для описания объекта, соответствующего отображенному в памяти порту ввода / вывода, или объекта, доступ к которому осуществляется с помощью асинхронно прерывающей функции. Действия над объявленными объектами не должны быть «оптимизированы» реализацией или переупорядочены, за исключением случаев, разрешенных правилами для оценки выражений.
Борисбн
2
@borisbn Несколько не по теме, но как узнать, что ваше устройство приняло команду перед отправкой новой команды?
user877329
3
@ user877329 Я видел это по поведению устройства, но это был отличный квест
borisbn