Может ли оптимизация компилятора вводить ошибки? - PullRequest
68 голосов
/ 27 апреля 2010

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

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

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

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

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

Ответы [ 23 ]

41 голосов
/ 27 апреля 2010

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

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

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

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

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

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

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

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

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

26 голосов
/ 28 апреля 2010

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

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

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

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

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

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

25 голосов
/ 27 апреля 2010

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

11 голосов
/ 27 апреля 2010

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

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

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

9 голосов
/ 27 апреля 2010

Для объединения других постов:

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

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

7 голосов
/ 27 апреля 2010

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

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

7 голосов
/ 27 апреля 2010

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

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

6 голосов
/ 27 апреля 2010

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

5 голосов
/ 27 апреля 2010

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

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

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

Заявленный:

Function foo( const type & tp ); 

Вызывается:

foo( foo2() );

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

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

5 голосов
/ 27 апреля 2010

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

...