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

С++: удвоения, точность, виртуальные машины и GCC

У меня есть следующий фрагмент кода:

#include <cstdio>
int main()
{
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

При компиляции с O3 с использованием gcc (4.4.4.5 и 4.6) и запуска изначально (ubuntu 10.10), он печатает ожидаемый результат "equal".

Однако тот же код, скомпилированный, как описано выше, и запускается на виртуальной машине (ubuntu 10.10, изображение виртуальной камеры), выводит "не равно" - это тот случай, когда установлены флаги O3 и O2, но не O1 и ниже. Когда компилируется с clang (O3 и O2) и запускается на виртуальной машине, я получаю правильный результат.

Я понимаю, что 1.1 не может быть правильно представлен с использованием double, и я читал "Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой", поэтому, пожалуйста, не указывайте мне, это, кажется, какая-то оптимизация, GCC делает это как-то не работает на виртуальных машинах.

Любые идеи?

Примечание. В стандарте С++ говорится, что продвижение по типу в этих ситуациях зависит от реализации, может быть, GCC использует более точное внутреннее представление, которое при применении теста неравенства является истинным - из-за дополнительной точности?

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

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   set_dpfpu();
   if ((1.0 + 0.1) != (1.0 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

UPDATE2:. Для тех, кто задает вопрос о характере кода выражения const, я изменил его следующим образом и все еще терпит неудачу при компиляции с помощью GCC. - но я полагаю, что оптимизатор может также преобразовывать следующее в выражение const.

#include <cstdio>
void set_dpfpu() { unsigned int mode = 0x27F; asm ("fldcw %0" : : "m" (*&mode)); 
int main()
{
   //set_dpfpu();  uncomment to make it work.
   double d1 = 1.0;
   double d2 = 1.0;  
   if ((d1 + 0.1) != (d2 + 0.1))
      printf("not equal\n");
    else
      printf("equal\n");
    return 0;
}

UPDATE3 Разрешение: Обновление виртуального бокса до версии 4.1.8r75467 разрешило проблему. Однако их остается одной проблемой, то есть: почему работала работа клана.

4b9b3361

Ответ 1

UPDATE: см. это сообщение Как справиться с избыточной точностью при вычислениях с плавающей запятой? В нем рассматриваются вопросы расширенной точности с плавающей запятой. Я забыл о расширенной точности в x86. Я помню моделирование, которое должно было быть детерминированным, но давало разные результаты для процессоров Intel, чем для процессоров PowePC. Причинами были расширенная точность архитектуры Intel.

Эта веб-страница рассказывает о том, как бросать процессоры Intel в режим округления с двойной точностью: http://www.network-theory.co.uk/docs/gccintro/gccintro_70.html.


Гарантирует ли виртуальный бокс то, что операции с плавающей запятой идентичны операциям с плавающей запятой? Я не мог найти такую ​​гарантию с быстрым поиском Google. Я также не нашел обещания, что vituralbox FP ops соответствует IEEE 754.

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

Если вы еще этого не сделали, отправьте вопрос forums.virtualbox.org и посмотрите, что об этом говорит сообщество.

Ответ 2

Да, это действительно странное поведение, но его можно легко объяснить:

В x86 регистры с плавающей запятой внутренне используют большую точность (например, 80 вместо 64). Это означает, что вычисление 1.0 + 0.1 будет вычисляться с большей точностью (а так как 1.1 не может быть представлено точно в двоичном виде при всех этих дополнительных битах, которые будут использоваться) в регистрах. Только при сохранении результата в памяти он будет усечен.

Это означает, что это просто: если вы сравните значение, загруженное из памяти, с новым значением, вычисленным в регистрах, вы получите "не равный" ответ, потому что одно значение было усечено, а другое - нет. Так что это не имеет ничего общего с VM/no VM, это просто зависит от кода, который генерирует компилятор, который может легко меняться, как мы видим там.

Добавьте его в растущий список сюрпризов с плавающей запятой.

Ответ 3

Я могу подтвердить одно и то же поведение вашего кода, отличного от VM, но поскольку у меня нет виртуальной машины, я не тестировал часть VM.

Однако компилятор, как Clang, так и GCC, будет оценивать постоянное выражение во время компиляции. См. Сборку ниже (используя gcc -O0 test.cpp -S):

    .file   "test.cpp"
    .section        .rodata
.LC0:
    .string "equal"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

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

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

ОБНОВЛЕНИЕ 1: (на основе "ОБНОВЛЕНИЯ 2" в исходном вопросе). Ниже представлена ​​сборка gcc -O0 -S test.cpp (для архитектуры с 64 битами). В нем вы можете увидеть строку movabsq $4607182418800017408, %rax дважды. Это будет для двух сравнительных флагов, я не проверял, но я полагаю, что значение $4607182418800017408 равно 1,1 в терминах с плавающей точкой. Было бы интересно скомпилировать это на виртуальной машине, если вы получите тот же результат (две аналогичные строки), тогда виртуальная машина будет делать что-то смешное во время выполнения, иначе это комбинация VM и компилятора.

main:
.LFB1:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $16, %rsp
        movabsq $4607182418800017408, %rax
        movq    %rax, -16(%rbp)
        movabsq $4607182418800017408, %rax
        movq    %rax, -8(%rbp)
        movsd   -16(%rbp), %xmm1
        movsd   .LC1(%rip), %xmm0
        addsd   %xmm1, %xmm0
        movsd   -8(%rbp), %xmm2
        movsd   .LC1(%rip), %xmm1
            addsd   %xmm2, %xmm1
        ucomisd %xmm1, %xmm0
        jp      .L6
        ucomisd %xmm1, %xmm0
        je      .L7

Ответ 4

Я вижу, вы добавили еще один вопрос:

Примечание. В стандарте С++ говорится, что продвижение по типу в этих ситуациях зависит от реализации, может быть, GCC использует более точное внутреннее представление, которое при применении теста неравенства является истинным - из-за дополнительной точности?

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

Или вы имели в виду совершенно новый внутренний формат для десятичных знаков? Нет, я отказываюсь верить в это. Это было бы не очень удобно, если бы это произошло.