Почему бы не всегда использовать оптимизацию компилятора? - PullRequest
33 голосов
/ 22 октября 2011

Один из вопросов, которые я задавал некоторое время назад , имел неопределенное поведение, поэтому оптимизация компилятора фактически приводила к сбою программы.

Но если в вашем коде нет неопределенного поведения, то есть ли причина не использовать оптимизацию компилятора? Я понимаю, что иногда для целей отладки может не потребоваться оптимизированный код (исправьте меня, если я ошибаюсь). Кроме этого, в производственном коде, почему бы не всегда использовать оптимизацию компилятора?

Кроме того, есть ли причина использовать, скажем, -O вместо -O2 или -O3?

Ответы [ 9 ]

29 голосов
/ 22 октября 2011

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

Как правило, когда я достигаю такого состояния, мне нравится делать комбинацию из:

  1. отладочная сборка (без оптимизации) и шагачерез код
  2. выдавил диагностические операторы в stderr, чтобы я мог легко отследить путь выполнения

Если ошибка более хитрая, я вынимаю valgrind и drd и, при необходимости, добавьте unit-тесты , чтобы изолировать проблему и убедиться, что при обнаружении проблемы решение работает должным образом.

В некоторых крайне редких случаях код отладки работает, но код выпуска не выполняется.Когда это происходит, почти всегда проблема в моем коде;Агрессивная оптимизация в сборках релиза может выявить ошибки, вызванные неправильным пониманием времени жизни временных и т. д. ... ... но даже в такой ситуации наличие отладочной сборки помогает локализовать проблемы.

Короче говоряЕсть несколько очень веских причин, по которым профессиональные разработчики создают и тестируют как отладочные (неоптимизированные), так и выпускаемые (оптимизированные) двоичные файлы.ИМХО, наличие как отладочных, так и релизных сборок, проходящих юнит-тесты в любое время, сэкономит вам много времени на отладку.

23 голосов
/ 22 октября 2011

Оптимизация компилятора имеет два недостатка:

  1. Оптимизация почти всегда перестраивает и / или удаляет код.Это снизит эффективность отладчиков, потому что больше не будет соответствия 1: 1 между вашим исходным кодом и сгенерированным кодом.Части стека могут отсутствовать, и пошаговое выполнение инструкций может привести к пропуску частей кода нелогичным образом.
  2. Оптимизация обычно обходится дорого, поэтому компиляция вашего кода займет больше времени при включенных оптимизацияхчем иначе.Трудно сделать что-то продуктивное, пока ваш код компилируется, поэтому очевидно, что более короткое время компиляции - хорошая вещь.

Некоторые из оптимизаций, выполняемых -O3, могут привести к увеличению размера исполняемых файлов.Это может быть нежелательно в некотором производственном коде.

Другая причина не использовать оптимизацию состоит в том, что используемый вами компилятор может содержать ошибки, которые существуют, только когда он выполняет оптимизацию.Компиляция без оптимизации может избежать этих ошибок.Если ваш компилятор содержит ошибки, лучшим вариантом может быть сообщение об этих ошибках / их исправление, переход на более качественный компилятор или написание кода, который полностью исключает эти ошибки.

Если вы хотите иметь возможность выполнятьотладка выпущенного производственного кода, тогда может быть хорошей идеей не оптимизировать код.

8 голосов
/ 22 октября 2011

3 причины

  1. Это сбивает с толку отладчик, иногда
  2. Это несовместимо с некоторыми шаблонами кода
  3. Не стоит: медленно или с ошибками, или занимает слишком много памяти, или создает слишком большой код.

В случае 2 представьте некоторый код ОС, который намеренно меняет типы указателей. Оптимизатор может предположить, что на объекты неправильного типа нельзя ссылаться, и сгенерировать код, который псевдоним изменяет значения памяти в регистрах и получает «неправильный» 1 ответ.

Случай 3 - интересная проблема. Иногда оптимизаторы делают код меньше, а иногда - больше. Большинство программ не сильно привязаны к процессору, и даже для тех программ, которые занимают всего 10% или менее кода, на самом деле требуется много вычислительных ресурсов. Если у оптимизатора есть какие-либо недостатки, то это выигрыш только для менее чем 10% программы.

Если сгенерированный код больше, он будет менее дружественным к кешу. Это могло бы стоить для библиотеки матричной алгебры с O (n 3 ) алгоритмами в крошечных маленьких циклах. Но для чего-то с более типичной временной сложностью переполнение кэша может фактически замедлить работу программы. Оптимизаторы можно настроить для всего этого, как правило, но, если программа, скажем, является веб-приложением, она, безусловно, будет более дружественной для разработчика, если компилятор просто сделает все универсальные действия и позволит разработчику просто не открывать фантазии ящик Пандоры.


1. Такие программы обычно не соответствуют стандартам, поэтому оптимизатор технически «корректен», но все еще не выполняет то, что разработчик намеревался.

3 голосов
/ 22 октября 2011

Причина в том, что вы разрабатываете одно приложение (отладочная сборка), а ваши клиенты запускают совершенно другое приложение (сборка выпуска). Если ресурсы тестирования невелики и / или используемый компилятор не очень популярен, я бы отключил оптимизацию для сборок релиза.

MS публикует многочисленные исправления для ошибок оптимизации в своем компиляторе MSVC x86. К счастью, я никогда не сталкивался с такой в ​​реальной жизни. Но это было не так с другими компиляторами. Компилятор SH4 в MS Embedded Visual C ++ был очень глючным.

2 голосов
/ 22 октября 2011

Две большие причины, которые я видел, проистекают из математики с плавающей точкой и чрезмерно агрессивного встраивания. Первое связано с тем, что математика с плавающей точкой крайне плохо определена стандартом C ++. Многие процессоры выполняют вычисления с использованием 80-битной точности, например, снижаясь до 64-битных, когда значение возвращается в основную память. Если версия подпрограммы часто сбрасывает это значение в память, а другая в конце получает значение только один раз, результаты вычислений могут немного отличаться. Простая настройка оптимизаций для этой подпрограммы может оказаться лучшим шагом, чем рефакторинг кода, чтобы сделать его более устойчивым к различиям.

Встраивание может быть проблематичным, поскольку по самой своей природе оно обычно приводит к созданию больших объектных файлов. Возможно, это увеличение размера кода неприемлемо по практическим соображениям: например, оно должно уместиться на устройстве с ограниченной памятью. Или, возможно, увеличение размера кода приводит к замедлению кода. Если процедура становится достаточно большой, чтобы она больше не помещалась в кэш, результирующие потери в кэше могут быстро перевесить преимущества, предоставляемые в первую очередь.

Я часто слышу о людях, которые при работе в многопоточной среде отключают отладку и сразу же сталкиваются с ордами новых ошибок из-за недавно обнаруженных условий гонки и еще много чего. Оптимизатор только что обнаружил здесь основной ошибочный код, поэтому отключать его в ответ, вероятно, не рекомендуется.

2 голосов
/ 22 октября 2011

Только что случилось со мной.Код, сгенерированный swig для взаимодействия с Java, корректен, но не будет работать с -O2 в gcc.

2 голосов
/ 22 октября 2011

Simple. Ошибки оптимизации компилятора.

0 голосов
/ 15 апреля 2014

Существует пример того, почему иногда опасно при использовании флага оптимизации, и наши тесты должны охватывать большую часть кода, чтобы заметить такую ​​ошибку.

Использование clang (потому что в gcc, даже без флага оптимизации, выполняется некоторая иптимизация, а вывод поврежден):

Файл: a.cpp

#include <stdio.h>

int puts(const char *str) {
    fputs("Hello, world!\n", stdout);
    return 1;
}

int main() {
    printf("Goodbye!\n");
    return 0;
}

Без флага -Ox:

> clang --output WithoutOptimization a.cpp;./withoutOptimization

> До свидания!

С флагом -Ox:

> clang - выход с O1 -O1 a.cpp;./withO1

> Привет, мир!

0 голосов
/ 22 октября 2011

Одним из примеров является логическая оценка короткого замыкания.Что-то вроде:

if (someFunc() && otherFunc()) {
  ...
}

«Умный» компилятор может понять, что someFunc всегда будет возвращать false по какой-то причине, в результате чего весь оператор оценивается как false, и решит не вызывать otherFunc для экономии времени ЦП.Но если otherFunc содержит некоторый код, который напрямую влияет на выполнение программы (возможно, он сбрасывает глобальный флаг или что-то в этом роде), он теперь не будет выполнять этот шаг, и ваша программа перейдет в неизвестное состояние.

...