x == x + 1 (типа микронаноисследование)
От: slava_phirsov Россия  
Дата: 23.08.11 06:52
Оценка: 14 (3) +1
Доброго времени суток всем читающим!

Заранее прошу извинить, если кому-то изложенное ниже покажется очевидным, но вопросик лично мне показался интересным, а ничего по теме не нагуглил — пришлось разбираться самому. Вот такая дурацкая задачка: сравнить между собой 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.
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.