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


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

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

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

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

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

22 67

22 ответа:

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

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

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

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

сертификат имеет презентацию называется" опасные оптимизации и потеря причинности " Роберта С. 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. компиляторы иногда имеют ошибки в своем коде, как и большинство программного обеспечения. Аргумент" умных людей " совершенно не имеет отношения к этому, так как спутники НАСА и другие приложения, созданные умными людьми, также имеют ошибки. Кодирование, которое выполняет оптимизацию, отличается от кодирования, которое этого не делает, поэтому, если ошибка находится в оптимизаторе, то действительно ваш оптимизированный код может содержать ошибки, в то время как ваш неоптимизированный код будет не.

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

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

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

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

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

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

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

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

при открытии сгенерированной сборки было ясно, что компилятор только что удалил 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 если вы строите с оптимизацией, добавьте другую переменную в метод, который называется аналогично существующей переменной того же типа в той же области, то одна из двух (новая или старая переменная) не будет действительна во время выполнения, и все ссылки на недопустимую переменную заменяются ссылками на другую.

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

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

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

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

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

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

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

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

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

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

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

не точно такой же код, но похожие.