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

Двойное умножение отличается от времени компиляции и времени выполнения на 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 приводит к неожиданному результату?, потому что здесь все значения двойные.

4b9b3361

Ответ 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

Ваши результаты могут незначительно отличаться.