Двойное умножение отличается от времени компиляции и времени выполнения на 32-битной платформе
Я компилирую и запускаю следующую программу на 32- и 64-разрядных платформах:
int main()
{
double y = 8.34214e08;
double z = 1.25823e45;
return y * z == 8.34214e08 * 1.25823e45;
}
В то время как в 64-битном результате ожидается ожидаемый (значения одинаковы, а код выхода не равен нулю) в 32 бит, кажется, что существует небольшая разница между значением, вычисленным во время компиляции, с правой стороны сравнения, и левая сторона вычисляется во время выполнения.
Является ли это ошибкой в компиляторе или существует логическое объяснение?
EDIT: это отличается от Почему сравнение double и float приводит к неожиданному результату?, потому что здесь все значения двойные.
Ответы
Ответ 1
IEEE-754 позволяет выполнять промежуточные вычисления с большей точностью (акцент мой).
(IEEE-754: 2008) "Языковой стандарт должен также определять и требовать реализации для предоставления атрибутов, которые позволяют и запрещают оптимизацию изменения стоимости, отдельно или коллективно для блока. Эти оптимизации могут включать, но не ограничено: [...] Использование более широких промежуточных результатов в оценке выражений.
В вашем случае, например, на IA-32, двойные значения могут быть сохранены в регистрах FPU x87 с большей точностью (80-бит вместо 64). Таким образом, вы на самом деле сравниваете умножение, выполняемое с двойной точностью, с умножением на двойную расширенную точность.
Например, на x64, где результат 1
(x90 FPU не используется, поскольку вместо него используется SSE), добавление опции gcc
-mfpmath=387
для использования x87 изменяет результат на 0
on моя машина.
И если вам интересно, разрешено ли это C, это:
(C99, 6.3.1.p8) "Значения плавающих операндов и результатов плавающих выражений могут быть представлены в большей точности и дальности, чем требуемые типом;"
Ответ 2
В общем, никогда не выполняйте проверки равенства с номерами с плавающей запятой. Вам нужно проверить, отличается ли результат, который вы хотите получить, от результата, который вы получаете меньше заданной точности.
То, что происходит здесь, по всей вероятности, связано с тем, что умножение выполняется на двух разных "платформах": один раз вашим кодом и один раз компилятором, который может иметь другую точность. Это происходит с наиболее компиляторами.
Ваша программа, вероятно, будет работать, если вы скомпилировали ее с помощью тех же параметров, которые были использованы для компиляции компилятора (предположим, что компилятор был скомпилирован сам по себе). Но это не означает, что вы получите правильный результат; вы получите ту же ошибку точности, которую получает компилятор.
(Кроме того, я предполагаю, что компилятор выполняет прямое умножение, а код распознавания, распознающий поплавки, не входит в уравнение. Это может быть принятие желаемого за действительное с моей стороны).
Тестирование
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib64/gcc/x86_64-suse-linux/4.8/lto-wrapper
Target: x86_64-suse-linux
Configured with: ../configure --prefix=/usr --infodir=/usr/share/info --mandir=/usr/share/man --libdir=/usr/lib64 --libexecdir=/usr/lib64 --enable-languages=c,c++,objc,fortran,obj-c++,java,ada --enable-checking=release --with-gxx-include-dir=/usr/include/c++/4.8 --enable-ssp --disable-libssp --disable-plugin --with-bugurl=http://bugs.opensuse.org/ --with-pkgversion='SUSE Linux' --disable-libgcj --disable-libmudflap --with-slibdir=/lib64 --with-system-zlib --enable-__cxa_atexit --enable-libstdcxx-allocator=new --disable-libstdcxx-pch --enable-version-specific-runtime-libs --enable-linker-build-id --enable-linux-futex --program-suffix=-4.8 --without-system-libunwind --with-arch-32=i586 --with-tune=generic --build=x86_64-suse-linux --host=x86_64-suse-linux
Thread model: posix
gcc version 4.8.3 20141208 [gcc-4_8-branch revision 218481] (SUSE Linux)
#include <stdio.h>
int main()
{
double y = 8.34214e08;
double z = 1.25823e45;
return printf("%s\n", y * z == 8.34214e08 * 1.25823e45 ? "Equal" : "NOT equal!");
}
Forcing -O0, чтобы избежать компиляции от оптимизации всего кода (спасибо @markgz!), получаем
$gcc -m32 -O0 -o float float.c && & & &./float
Не равный!
$ gcc -m32 -frounding-math -O0 -o float float.c && & & &./float
Равно
Для записи, поскольку вы добрались туда передо мной: -),
-frounding-математик
Отключить преобразования и оптимизации, которые предполагают поведение округления с плавающей запятой по умолчанию. Это округло до нуля для всех с плавающей точкой на целые преобразования и от округлой до ближайшей для всех другие арифметические усечения. Этот параметр должен быть указан для программы, которые изменяют режим округления FP динамически или могут быть выполненный с использованием режима округления по умолчанию. Эта опция отключает постоянное сгибание выражений с плавающей запятой во время компиляции (что может влиять на режим округления) и арифметические преобразования, которые небезопасны при наличии зависящих от знака режимов округления.
По умолчанию используется значение -fno-rounding-math.
Ответ 3
Расчеты с плавающей точкой, выполняемые во время компиляции, часто происходят с большей точностью, чем double
использует во время выполнения. Кроме того, C может выполнять промежуточные промежуточные вычисления double
при более высокой точности long double
. Объясните свое неравенство. Подробнее см. FLT_EVAL_METHOD
.
volatile double y = 8.34214e08;
volatile double z = 1.25823e45;
volatile double yz = 8.34214e08 * 1.25823e45;
printf("%.20e\n", y);
printf("%.20e\n", z);
printf("%.20e\n", yz);
printf("%.20Le\n", (long double) y*z);
printf("%.20Le\n", (long double) 8.34214e08 * 1.25823e45);
8.34214000000000000000e+08
1.25822999999999992531e+45
// 3 different products!
1.04963308121999993395e+54
1.04963308121999993769e+54
1.04963308122000000000e+54
Ваши результаты могут незначительно отличаться.