Правило как-если-бы
Разрешает любые преобразования кода, которые не изменяют наблюдаемое поведение программы.
Объяснение
Компилятору C++ разрешено вносить любые изменения в программу, пока выполняется следующее:
|
1) В каждой точке последовательности, значения всех объектов volatile стабильны (предыдущие оценки завершены, новые оценки не начаты)
|
(до C++11) |
|
1) Доступ (чтение и запись) к объектам volatile происходит строго в соответствии с семантикой выражений, в которых они встречаются. В частности, они не переупорядочены по отношению к другим volatile доступам в том же потоке.
|
(начиная с C++11) |
ON, изменения в среде с плавающей запятой (исключения с плавающей запятой и режимы округления) гарантированно соблюдаются арифметическими операторами с плавающей запятой и вызовами функций, как если бы они выполнялись как написано, за исключением того, что
- результат любого выражения с плавающей запятой, кроме приведения типов и присваивания, может иметь диапазон и точность типа с плавающей запятой, отличные от типа выражения (смотрите FLT_EVAL_METHOD)
- несмотря на вышесказанное, промежуточные результаты любого выражения с плавающей запятой могут быть вычислены с бесконечным диапазоном и точностью (если
#pragma STDC FP_CONTRACT не равна
OFF)
Примечание
Поскольку компилятор (обычно) не может анализировать код внешней библиотеки, чтобы определить, выполняет она или не выполняет ввод-вывод или volatile доступ, на вызовы сторонних библиотек оптимизация также не влияет. Однако стандартные вызовы библиотеки могут быть заменены другими вызовами, исключены или добавлены в программу во время оптимизации. Статически связанный код сторонней библиотеки может быть оптимизирован во время компоновки.
Программы с неопределённым поведением, например из-за доступа к массиву за пределами границ, модификации константного объекта, нарушений порядка оценки и т.д., свободны от правила как-если-бы: они часто меняют наблюдаемое поведение при перекомпиляции с другими настройками оптимизации. Например, если проверка на переполнение целого числа со знаком полагается на результат этого переполнения, if(n+1 < n) abort();, некоторыми компиляторами это выражение полностью удаляется, потому что знаковое переполнение является неопределённым поведением, и оптимизатор может предположить, что этого никогда не произойдёт, и тест является избыточным.
Пропуск копирования является исключением из правила как-если-бы: компилятор может удалять вызовы конструкторов перемещения и копирования и соответствующие вызовы деструкторов временных объектов, даже если эти вызовы имеют наблюдаемые побочные эффекты.
|
Выражение new имеет ещё одно исключение из правила как-если-бы: компилятор может удалить вызовы заменяемых функций распределения, даже если определённая пользователем замена предоставляется и имеет наблюдаемые побочные эффекты. |
(начиная с C++14) |
Количество и порядок исключений с плавающей запятой можно изменить с помощью оптимизации, если состояние, наблюдаемое следующей операцией с плавающей запятой, такое же, как если бы не было никакой оптимизации:
#pragma STDC FENV_ACCESS ON
for (i = 0; i < n; ++i) x + 1; // x+1 мёртвый код, но может вызывать FP исключения
// (если оптимизатор не докажет обратное). Однако его выполнение n раз вызывает
// одно и то же исключение снова и снова. Таким образом, это можно оптимизировать так:
if (0 < n) x + 1;
Пример
int& preinc(int& n) { return ++n; }
int add(int n, int m) { return n+m; }
// volatile ввод для предотвращения константного сворачивания
volatile int input = 7;
// volatile вывод, чтобы сделать результат видимым побочным эффектом
volatile int result;
int main()
{
int n = input;
// использование встроенных операторов вызовет неопределённое поведение
// int m = ++n + ++n;
// но использование функций гарантирует, что код выполняется так,
// как-если-бы функции не перекрывались
int m = add(preinc(n), preinc(n));
result = m;
}
Вывод:
# полный код функции main(), созданный компилятором GCC
# x86 (Intel) платформа:
movl input(%rip), %eax # eax = input
leal 3(%rax,%rax), %eax # eax = 3 + eax + eax
movl %eax, result(%rip) # result = eax
xorl %eax, %eax # eax = 0 (возвращаемое значение main())
ret
# PowerPC (IBM) платформа:
lwz 9,LC..1(2)
li 3,0 # r3 = 0 (возвращаемое значение main())
lwz 11,0(9) # r11 = input;
slwi 11,11,1 # r11 = r11 << 1;
addi 0,11,3 # r0 = r11 + 3;
stw 0,4(9) # result = r0;
blr
# Sparc (Sun) платформа:
sethi %hi(result), %g2
sethi %hi(input), %g1
mov 0, %o0 # o0 = 0 (возвращаемое значение main)
ld [%g1+%lo(input)], %g1 # g1 = input
add %g1, %g1, %g1 # g1 = g1 + g1
add %g1, 3, %g1 # g1 = 3 + g1
st %g1, [%g2+%lo(result)] # result = g1
jmp %o7+8
nop
# во всех случаях побочные эффекты preinc() были устранены,
# а вся функция main() была уменьшена до эквивалента result = 2*input + 3;
Смотрите также
Документация C по правило как-если-бы
|