Доброго времени суток всем читающим!
Заранее прошу извинить, если кому-то изложенное ниже покажется очевидным, но вопросик лично мне показался интересным, а ничего по теме не нагуглил — пришлось разбираться самому. Вот такая дурацкая задачка: сравнить между собой X и X+1, где X — ну очень большое вещественное число с плавающей точкой. Казалось бы, ну какие тут могут быть подводные камни? Вот код:
#include <stdio.h>
int foo(double x)
{
return (x + 1.0) == x;
}
int bar(double x)
{
double y = x + 1.0;
return x == y;
}
int main(int argc, char* argv[])
{
const double eps = 1e+16;
printf("%i-%i\n", foo(eps), bar(eps));
return 0;
}
Компилятор — gcc-4.4.5, архитектура x86. Ключи компиляции: Wall, pedantic, ansi, плюс оптимизация от O0 до O3. Казалось бы, мы должны получить на консоль
1-1
.
Ан нет, для разных ключей оптимизации получаем вот что (для ключей O0..O3, соответственно):
0-1
0-0
1-1
1-1
Ну, делать нечего, после неудачного гугленья полез в ассемблерный код (поскольку компилятор GCC, то и ассемблерный синтаксис, извините, AT&T)...
Для ключа O0 (полное отсутствие оптимизации):
foo:
;...
; загрузили в стек сопроцессора копию аргумента функции
fldl -8(%ebp)
; загрузили в стек сопроцессора 1.0
fld1
; сложили, занесли результат в st(1)
faddp %st, %st(1)
; снова(!) загрузили в стек сопроцессора аргумент функции
fldl -8(%ebp)
; теперь сравниваем x и x + 1,
; а результат сравнения - в регистр флагов процессора...
fucomip %st(1), %st
;....
bar:
;...
fldl -24(%ebp)
fld1
faddp %st, %st(1)
; честно сохраняем результат сложения в стеке системы
fstpl -8(%ebp)
; снова загружаем x и x + 1 в стек сопроцессора и сравниваем
fldl -24(%ebp)
fldl -8(%ebp)
fucomip %st(1), %st
;...
Итак, что получается? Я априори считал, что в функции foo результат сложения будет записан во временную переменную, а потом эта переменная будет сравниваться с исходным значением. Но хитрый компилятор использовал для хранения временной переменной стек сопроцессора, т.е. число не одинарной, а расширенной точности, которому наше 1e+16 — что слону дробина. В функции bar — все честно, мы явно записали результат сложения во временную переменную, и компилятор сделал ровно то, о чем попросили.
В случае ключа O1 код, сгенерированный и для foo, и для bar практически одинаков и совпадает с кодом, сгенерированным для foo при ключе оптимизации O0: никаких временных переменных в стеке системы, для хранения результата сложения используется стек сопроцессора. С очевидным результатом.
Сложнее понять случаи с ключами O2 и O3. Код, сгенерированный для них одинаков. Код, сгенерированный для функций foo, и bar, мало чем отличается друг от друга и от (па-па-па-пам!) кода, сгенерированного для функции foo в случае ключа O1 (то есть, опять-таки результат сложения хранится в стеке сопроцессора, а временная переменная y игнорируется). WTF? Почему в случае ключа O1 мы получаем 0, а в случаях ключей O2 и O3 — 1? А проблема, оказывается, совсем в другом месте, говоря конкретно — в функции main. Вот что наделал хитрый компилятор:
main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $16, %esp
movl $1, 12(%esp)
movl $1, 8(%esp)
movl $.LC2, 4(%esp)
movl $1, (%esp)
call __printf_chk
xorl %eax, %eax
leave
ret
А где же вызовы foo и bar? А их там нет. Компилятор сам решил, чт
о должно получиться в итоге. Без их помощи. И даже добавление в функции foo и bar кода, дающего сторонние эффекты, например, вызова printf, помогает только c опцией компилятора O2. С опцией O3 вызов printf вставляется непосредственно в функцию main.
Вот, все. Замечания are welcome.
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)