C ++: дубли, точность, виртуальные машины и GCC - PullRequest
21 голосов
/ 18 января 2012

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

#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), он выводит ожидаемый результат «равно».

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

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

Есть идеи?

Примечание. Стандарт C ++ гласит, что продвижение типов в этих ситуациях зависит от реализации. Может ли быть, что 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 Разрешение: Обновление virtualbox до версии 4.1.8r75467 решило проблему. Однако остается одна проблема: почему работала сборка clang.

Ответы [ 4 ]

10 голосов
/ 19 января 2012

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

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


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

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

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

5 голосов
/ 19 января 2012

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

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

Что это значит просто: если вы сравните значение, загруженное из памяти, со значением, вновь вычисленным в регистрах, вы получите «неравный»назад, потому что одно значение было усечено, а другое нет.Так что это не имеет ничего общего с ВМ / без ВМ, это просто зависит от кода, сгенерированного компилятором, который может легко колебаться, как мы видим там.

4 голосов
/ 19 января 2012

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

Однако компилятор, 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

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

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

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

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
2 голосов
/ 19 января 2012

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

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

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

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

...