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

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

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

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

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

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

68
задан tshepang 19 January 2016 в 09:36
поделиться

17 ответов

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

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

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

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

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

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

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

     // не выполняется, потому что тест переполнения удаляется 
    if (ptr + len  max) return EINVAL; 
     
  • Использование искусства переполнения вообще:

     // Компилятор оптимизирует это до бесконечного цикла 
    for (i = 1 ; i> 0; i + = i) ++ j; 
     
  • Очистка памяти от конфиденциальной информации:

     // компилятор может удалить эти «бесполезные записи» 
    memset (password_buffer , 0, sizeof (password_buffer)); 
     

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

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

41
ответ дан 24 November 2019 в 14:13
поделиться

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

25
ответ дан 3 July 2019 в 18:54
поделиться

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

1
ответ дан 24 November 2019 в 14:13
поделиться

Это может случиться. Это затронуло даже Linux .

2
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

Объявлено:

Function foo( const type & tp ); 

Вызывается:

foo( foo2() );

где foo2 () возвращает объект класса типа ;

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

5
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

10
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

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

2
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

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

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

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

29
ответ дан 24 November 2019 в 14:13
поделиться

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

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

7
ответ дан 24 November 2019 в 14:13
поделиться

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

6
ответ дан 24 November 2019 в 14:13
поделиться

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

5
ответ дан 24 November 2019 в 14:13
поделиться

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

Это , по-видимому, исправлено с тех пор.

7
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

12
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

2
ответ дан 24 November 2019 в 14:13
поделиться

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

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

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

3
ответ дан 24 November 2019 в 14:13
поделиться

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

0
ответ дан 24 November 2019 в 14:13
поделиться

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

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

4
ответ дан 24 November 2019 в 14:13
поделиться
Другие вопросы по тегам:

Похожие вопросы: