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

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

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

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

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

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

Ответ 1

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

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

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

Другой пример: Ядро linux securecoding.cert.org есть документ под названием "Опасные оптимизации и потеря причинности" Роберта С. Сикорд, в котором перечислены много оптимизаций, которые вводят (или выставляют) ошибки в программах. ссылка Google Cache. В нем обсуждаются различные виды оптимизаций, которые возможны: "делать то, что аппаратное обеспечение", чтобы "ловить все возможные действия undefined", чтобы "делать все, что не запрещено".

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

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

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

    // 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 - это контракт между разработчиками и компиляторами, но условия соглашения могут меняться со временем, а не все понимают каждую деталь или согласны с тем, что детали даже разумны.

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

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

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

Заявлен:

Function foo( const type & tp ); 

Вызывается:

foo( foo2() );

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

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

Ответ 11

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

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

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

Ответ 12

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

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

Ответ 13

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

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

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

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

Ответ 14

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

Ответ 15

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

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

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

Ответ 16

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

Ответ 17

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

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

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

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

Ответ 18

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

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

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

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

Ответ 19

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

Ответ 20

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

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

Ответ 21

У меня была проблема с .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(); }

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

Ответ 22

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