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
您的结果可能略有不同。
我正在 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
您的结果可能略有不同。