Подтвердить что ты не робот

Могут ли разные уровни оптимизации привести к функционально другому коду?

Мне интересно о свободах, которые компилятор имеет при оптимизации. Ограничьте этот вопрос до GCC и C/С++ (любая версия, любой вкус стандарта):

Можно ли написать код, который ведет себя по-разному в зависимости от того, какой уровень оптимизации он был скомпилирован?

Примером, который я имею в виду, является печать различных битов текста в различных конструкторах на С++ и получение различий в зависимости от того, были ли удалены копии (хотя я не смог заставить такую ​​работу работать).

Подсчет тактовых циклов не разрешен. Если у вас есть пример для компилятора, отличного от GCC, мне тоже будет любопытно, но я не могу его проверить. Бонусные баллы для примера в C.: -)

Изменить: код примера должен быть стандартным и не содержать поведение undefined с самого начала.

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

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

4b9b3361

Ответ 1

Часть применяемого стандарта С++ является § 1.9 "Исполнение программы". В нем, в частности, говорится:

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

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

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

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

EDIT: Позвольте мне исправить мой вывод: Да, код может вести себя по-разному на разных уровнях оптимизации, если каждое поведение, как правило, идентично одному из вариантов поведения стандартной абстрактной машины.

Ответ 2

Можно ли написать код, который ведет себя по-разному, в зависимости от того, уровень оптимизации был скомпилирован с?

Только если вы вызываете ошибку компилятора.

ИЗМЕНИТЬ

Этот пример ведет себя по-разному на gcc 4.5.2:

void foo(int i) {
  foo(i+1);
}

main() {
  foo(0);
}

Скомпилированный с помощью -O0 создает сбой программы с ошибкой сегментации.
Скомпилированный с помощью -O2 создает программу, вступающую в бесконечный цикл.

Ответ 3

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

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

И как вы уже упоминали, побочные эффекты в конструкторах копий могут исчезнуть при изменении уровней оптимизации.

Ответ 4

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

В целях различного поведения на разных уровнях оптимизации "уровень оптимизации A" должен обозначать gcc -O0 (я использую версию 4.3.4, но это не имеет большого значения, я думаю, что любая даже смутно недавняя версия будет показать разницу, которую я получаю после этого), а "уровень оптимизации B" означает gcc -O0 -fno-elide-constructors.

Код прост:

#include <iostream>

struct Foo {
    ~Foo() { std::cout << "~Foo\n"; }
};

int main() {
    Foo f = Foo();
}

Выход на уровне оптимизации A:

~Foo

Выход на уровне оптимизации B:

~Foo
~Foo

Код полностью легален, но результат зависит от реализации из-за исключения конструктора экземпляра и, в частности, он чувствителен к флагом оптимизации gcc, который отключает копирование ctor elision.

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

Ответ 5

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

  • undefined поведение не должно быть совместимый между различными компиляторами выполнение или выполнение неисправного кода Операции с плавающей запятой могут выполняться
  • различное округление
  • аргументы для вызовов функций могут быть оценивается в любом порядке
  • выражения с volatile квалифицированными тип может быть или не быть оценен просто для их побочных эффектов
  • идентичные const квалифицированные составные литералы могут или не могут быть свернуты в одну статическую ячейку памяти

Ответ 6

Все, что есть Undefined Поведение в соответствии со стандартом может изменить его поведение в зависимости от уровня оптимизации (или фазы луны, если на то пошло).

Ответ 7

Параметр -fstrict-aliasing может легко вызывать изменения в поведении, если у вас есть два указателя на один и тот же блок памяти. Это должно быть недействительным, но на самом деле довольно распространено.

Ответ 8

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

Ответ 9

Эта программа C вызывает поведение undefined, но отображает разные результаты на разных уровнях оптимизации:

#include <stdio.h>
/*
$ for i in 0 1 2 3 4 
    do echo -n "$i: " && gcc -O$i x.c && ./a.out 
  done
0: 5
1: 5
2: 5
3: -1
4: -1
*/

void f(int a) {
  int b;
  printf("%d\n", (int)(&a-&b));
}
int main() {
 f(0);
 return 0;
}

Ответ 10

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

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