С++: удвоения, точность, виртуальные машины и 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 разрешило проблему. Однако их остается одной проблемой, то есть: почему работала работа клана.
Ответы
Ответ 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
.
Или вы имели в виду совершенно новый внутренний формат для десятичных знаков? Нет, я отказываюсь верить в это. Это было бы не очень удобно, если бы это произошло.