Может ли оптимизация компилятора ввести ошибки?

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

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

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

Он не убедил меня, но я должен признаюсь, мне не хватает реальных примеров, чтобы подкрепить свою точку зрения.

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

22 ответов


оптимизация компилятора может привести к ошибкам или нежелательному поведению. Поэтому их можно отключить.

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

обновление: как отметил Адам Робинсон в комментарии, сценарий, который я описал выше, является скорее ошибкой программирования, чем ошибкой оптимизатора. Но я пытаюсь показать, что некоторые программы, которые правильно, в сочетании с некоторыми оптимизации, которые в противном случае работают правильно, могут вводить ошибки в программу, когда они объединены вместе. В некоторых случаях спецификация языка говорит:" Вы должны делать это так, потому что эти виды оптимизации могут произойти, и ваша программа потерпит неудачу", и в этом случае это ошибка в коде. Но иногда компилятор имеет (обычно необязательную) функцию оптимизации, которая может генерировать неправильный код, потому что компилятор слишком сильно пытается оптимизировать код или не может обнаружить, что оптимизация неуместна. В этом случае программист должен знать, когда безопасно включить рассматриваемую оптимизацию.

другой пример: The ядро linux имело ошибку где потенциально нулевой указатель разыменовывался перед тестом для этого указателя, являющегося нулевым. Однако в некоторых случаях можно было сопоставить память с нулевым адресом, что позволило добиться успеха в разыменовании. Компилятор, заметив, что указатель был разыменован, предположил, что он не может быть NULL, а затем удалить нулевой тест позже и весь код в этой ветви. это ввело уязвимость безопасности в код, поскольку функция будет использовать недопустимый указатель, содержащий данные, предоставленные злоумышленником. Для случаев, когда указатель был законно нулевым, а память не была сопоставлена с нулевым адресом, ядро все равно будет OOPS, как и раньше. Таким образом, до оптимизации код содержал одну ошибку; после нее содержалось две, и одна из них разрешала локальный корень эксплуатировать.

сертификат имеет презентацию называется "опасные оптимизации и потеря причинности" Робертом С. Seacord, который перечисляет много оптимизаций, которые вводят (или подвергают) ошибки в программах. В нем обсуждаются различные виды оптимизации, которые возможны, от "делать то, что делает оборудование" до "ловушки все возможное неопределенное поведение", чтобы "делать все, что не запрещено".

некоторые примеры кода, который отлично подходит до агрессивно-оптимизирующий компилятор получает его в свои руки:

  • проверка на переполнение

    // fails because the overflow test gets removed
    if (ptr + len < ptr || ptr + len > max) return EINVAL;
    
  • использование переполнения artithmetic вообще:

    // The compiler optimizes this to an infinite loop
    for (i = 1; i > 0; i += i) ++j;
    
  • Очистка памяти конфиденциальной информации:

    // the compiler can remove these "useless writes"
    memset(password_buffer, 0, sizeof(password_buffer));
    

проблема здесь в том, что компиляторы на протяжении десятилетий были менее агрессивными в оптимизации, и поэтому поколения программистов C изучают и понимают такие вещи, как двойки фиксированного размера дополняют сложение и то, как оно переполняется. Затем стандарт языка C изменяется разработчиками компиляторов, и тонкие правила меняются, несмотря на то, что оборудование не меняется. Спецификация языка C - это контракт между разработчиками и компиляторами, но условия соглашения могут меняться со временем, и не все понимают каждую деталь или согласны с тем, что детали даже разумны.

вот почему большинство компиляторов предлагают флаги для выключения (или включения) процессы оптимизации. Ваша программа написана с пониманием того, что целые числа могут переполняться? Затем вы должны отключить оптимизацию переполнения, потому что они могут вводить ошибки. Ваша программа строго избегает сглаживания указателей? Затем вы можете включить оптимизации, которые предполагают, что указатели никогда не сглаживаются. Ваша программа пытается очистить память, чтобы избежать утечки информации? О, в этом случае вам не повезло: вам либо нужно отключить удаление мертвого кода, либо вам нужно знать заранее, что ваш компилятор собирается устранить ваш "мертвый" код и использовать для него некоторую работу.


Да, абсолютно.
См.здесь, здесь (который все еще существует - "дизайн"!?!), здесь, здесь, здесь, здесь...


когда ошибка уходит, отключив оптимизацию, большую часть времени это все еще ваша ошибка

Я отвечаю за коммерческое приложение, написанное в основном на C++ - началось с VC5, перенесено на VC6 раньше, теперь успешно перенесено на VC2008. За последние 10 лет он вырос до более чем 1 миллиона строк.

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

Так почему я жалуюсь? Потому что в то же время были десятки ошибок, которые заставили меня усомниться в компиляторе - но это оказалось моим недостаточным пониманием стандарта C++. Стандарт предоставляет место для оптимизации, которую компилятор может или не может использовать.

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

Почему я отвечаю так поздно: прекратите обвинять компилятор, прежде чем вы подтвердите, что это на самом деле ошибка компилятора.


оптимизация компилятора (и среды выполнения), безусловно, может ввести нежелательное поведение-но это по крайней мере должны только если вы полагаетесь на неопределенное поведение (или действительно делаете неправильные предположения о хорошо определенном поведении).

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

предполагая, что вы включаете JITs в качестве компиляторов, я видел ошибки в выпущенных версиях как .NET JIT, так и Hotspot JVM (у меня нет деталей на данный момент, к сожалению), которые были воспроизводимы в особенно странных ситуациях. Были ли они связаны с определенными оптимизациями или нет, я не знаю.


чтобы объединить другие сообщения:

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

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


только один пример: несколько дней назад кто-то обнаружен что gcc 4.5 с опцией -foptimize-sibling-calls (который подразумевается -O2) создает исполняемый файл Emacs, который segfaults при запуске.

Это по всей видимости, были исправлены так.


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

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


да. Хорошим примером является шаблон блокировки с двойной проверкой. В C++ нет способа безопасно реализовать блокировку с двойной проверкой, потому что компилятор может переупорядочивать инструкции способами, которые имеют смысл в однопоточной системе, но не в многопоточной. Полное обсуждение можно найти вhttp://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf


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


я столкнулся с этим несколько раз с новым компилятором, создающим старый код. Старый код будет работать, но в некоторых случаях полагается на неопределенное поведение, например, неправильно определенную перегрузку оператора / cast. Он будет работать в сборке отладки VS2003 или VS2005, но в выпуске он потерпит крах.

Открытие сгенерированной сборки было ясно, что компилятор только что удалил 80% функциональности рассматриваемой функции. Перезапись кода, чтобы не использовать неопределенное поведение, очистила его вверх.

более очевидный пример: VS2008 vs GCC

заявил:

Function foo( const type & tp ); 

под названием:

foo( foo2() );

здесь foo2() возвращает объект класса type;

имеет тенденцию к сбою в GCC, потому что объект не выделяется в стеке в этом случае, но VS делает некоторую оптимизацию, чтобы обойти это, и это, вероятно, сработает.


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

чтобы включить такую оптимизацию предсказуемым образом, стандарт ISO для языка программирования C (включая его более новую версию C99) указывает, что незаконно (за некоторыми исключениями) для указателей разных типов ссылаться на одно и то же расположение памяти. Это правило, известное как " строгое сглаживание", позволяет впечатляющее увеличение производительности[нужная цитата], но, как известно, сломать некоторый в противном случае действительный код. Несколько программных проектов намеренно нарушают эту часть стандарта C99. Например, Python 2.x сделал это для реализации подсчета ссылок[1] и необходимых изменений в базовых структурах объектов в Python 3, чтобы включить эту оптимизацию. Ядро Linux делает это, потому что строгое сглаживание вызывает проблемы с оптимизацией встроенного кода.[2] в таких случаях при компиляции с gcc, опция-fno-strict-aliasing вызывается для предотвращения нежелательных или недопустимых оптимизаций, которые могут привести к неправильному коду.


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

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

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


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

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

делает глупый аргумент.

Итак, пока у вас нет оснований полагать, что компилятор делает это, зачем позировать об этом?


Я, конечно, согласен, что глупо говорить, потому что компиляторы написаны "умными людьми", что они поэтому непогрешимы. Умные люди спроектировали мост Гинденберг и Такома-Нэрроуз. Даже если это правда, что составители компиляторов являются одними из самых умных программистов, это также правда, что компиляторы являются одними из самых сложных программ. Конечно у них есть ошибки.

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

Так, в общие сведения: доверяйте компилятору. Но разве они когда-нибудь ошибаются? Конечно.


Это может случиться. Это даже повлияло Linux.


насколько я помню, в начале Delphi 1 была ошибка, когда результаты Min и Max были отменены. Была также неясная ошибка с некоторыми значениями с плавающей запятой только тогда, когда значение с плавающей запятой использовалось в dll. По общему признанию, прошло больше десяти лет, поэтому моя память может быть немного нечеткой.


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

Так, например, если у меня АВСD типа MyCustomClass и я abdc типа MyCustomClass и я АВСD.a=5 и abdc.a=7 тогда обе переменные будут иметь свойство a=7. Чтобы устранить проблему, обе переменные должны быть удалены, программа скомпилирована (надеюсь, без ошибок), затем они должны быть повторно добавлены.

Я думаю, что я столкнулся с этой проблемой несколько раз с .NET 4.0 и C# при выполнении приложений Silverlight. На моей последней работе мы часто сталкивались с проблемой на C++. Возможно, это было потому, что компиляции заняли 15 минут, поэтому мы будем создавать только необходимые библиотеки, но иногда оптимизированные код был точно таким же, как и предыдущая сборка, хотя был добавлен новый код и никаких ошибок сборки не сообщалось.

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


оптимизация компилятора может выявить (или активировать) спящие (или скрытые) ошибки в вашем коде. В вашем коде C++ может быть ошибка, о которой вы не знаете, которую вы просто не видите. В этом случае это скрытая или Спящая ошибка, потому что эта ветвь кода не выполняется [достаточное количество раз].

вероятность ошибки в коде гораздо больше (в тысячи раз больше), чем ошибка в коде компилятора: потому что компиляторы проходят тщательную проверку. По ТДД плюс практически всеми людьми, которые используют их с момента их освобождения!). Таким образом, практически маловероятно, что ошибка обнаружена вами и не обнаружена буквально сотни тысяч раз, когда она используется другими людьми.

A скрытые ошибки или скрытая ошибка - это просто ошибка, которая еще не раскрыта программисту. Люди, которые могут утверждать, что их код C++ нет (скрытых) ошибок очень редки. Это требует знаний c++ (очень немногие могут претендовать на это) и обширных тестирование кода. Речь идет не только о программисте, но и о самом коде (стиле разработки). Быть подверженным ошибкам-это характер кода (насколько строго он тестируется) или/и программист (насколько дисциплинирован в тесте и насколько хорошо знает C++ и программирование).

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


все, что вы можете себе представить, делая С или к программе будет вводить ошибки.


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

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


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


у меня была проблема с .net 4 вчера с чем-то, что выглядит...

double x=0.4;
if(x<0.5) { below5(); } else { above5(); }

и он бы назвал above5(); но если я использую x где-нибудь, он бы назвал below5();

double x=0.4;
if(x<0.5) { below5(); } else { System.Console.Write(x); above5(); }

не тот же самый код, но похожий.