32 位平台中编译时和运行时的双乘法不同

Double multiplication differs between compile time and runtime in 32 bit platform

我正在 32 位和 64 位平台上编译并运行以下程序:

int main()
{
  double y = 8.34214e08;
  double z = 1.25823e45;

  return y * z == 8.34214e08 * 1.25823e45;
}

虽然在 64 位中结果是预期的(值相同且退出代码不为零),但在 32 位中似乎在编译时计算的值之间存在一些差异,右手边比较,左侧在运行时计算。

这是编译器中的错误还是有合理的解释?

编辑:这与 Why comparing double and float leads to unexpected result? 不同,因为这里所有的值都是双倍的。

一般来说,从不equality checks with floating point numbers。您需要检查您想要的结果与您得到的结果是否相差小于预设精度。

这里发生的事情很可能是由于乘法是 运行 在两个不同的 "platforms" 上:一次是你的代码,一次是编译器,它们可能有不同的精度。 most compilers.

会发生这种情况

如果您使用用于编译编译器的相同选项编译它,您的程序可能工作(假设编译器是自己编的)。但这并不意味着您会得到 正确的 结果;您将得到与编译器相同的精度错误。

(此外,我假设编译器执行直接乘法 识别浮点数的解析代码不进入等式。这很可能是我的一厢情愿部分)。

测试

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!");
}

强制-O0 以避免编译器优化整个代码(感谢@markgz!),我们得到

$ gcc -m32 -O0 -o float float.c && ./float 不相等! $ gcc -m32 -frounding-math -O0 -o float float.c && ./float 等于

郑重声明,因为你比我先到 :-),

-frounding-math

Disable transformations and optimizations that assume default floating-point rounding behavior. This is round-to-zero for all floating point to integer conversions, and round-to-nearest for all other arithmetic truncations. This option should be specified for programs that change the FP rounding mode dynamically, or that may be executed with a non-default rounding mode. This option disables constant folding of floating-point expressions at compile time (which may be affected by rounding mode) and arithmetic transformations that are unsafe in the presence of sign-dependent rounding modes.

The default is -fno-rounding-math.

IEEE-754 允许以更高的精度进行中间计算(强调我的)。

(IEEE-754:2008) "A language standard should also define, and require implementations to provide, attributes that allow and disallow value-changing optimizations, separately or collectively, for a block. These optimizations might include, but are not limited to: [...] Use of wider intermediate results in expression evaluation."

在您的情况下,例如在 IA-32 上,双精度值可以更精确地存储在 x87 FPU 寄存器中(80 位而不是 64 位)。所以你实际上是在比较双精度乘法和双扩展精度乘法。

例如,在结果为 1 的 x64 上(不使用 x87 FPU,而是使用 SSE),添加 gcc 选项 -mfpmath=387 以使用 x87结果在我的机器上变为 0

如果您想知道 C 是否也允许这样做,那就是:

(C99, 6.3.1.p8) "The values of floating operands and of the results of floating expressions may be represented in greater precision and range than that required by the type;"

在编译时完成的浮点计算通常比 double 在 运行 时使用的精度更高。 C 也可以以更高的 long double 精度执行 运行 时间中间 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

您的结果可能略有不同。