6

我正在将我的应用程序从 32 位移植到 64 位。目前,代码在两种架构下都可以编译,但结果不同。由于各种原因,我使用浮点数而不是双精度数。我假设在一台机器上而不是另一台机器上发生了一些从浮点到双的隐式上转换。有没有办法控制这个,或者我应该寻找的特定陷阱?

编辑添加:

32位平台

 gcc (GCC) 4.1.2 20070925 (Red Hat 4.1.2-33)
 Dual-Core AMD Opteron(tm) Processor 2218 HE

64位平台

 gcc (Ubuntu 4.3.3-5ubuntu4) 4.3.3
 Intel(R) Xeon(R) CPU

应用 -mfpmath=387 会有所帮助,在算法的 1 次迭代之后,值是相同的,但除此之外,它们再次不同步。

我还应该补充一点,我担心的不是结果不一样,而是移植到 64 位平台发现了我不知道的 32 位依赖关系。

4

10 回答 10

11

假设 x86-64,您的编译器可能使用 SSE 操作码在 64 位平台上执行其大部分浮点运算,而出于兼容性原因,它之前可能使用 FPU 进行许多操作。

SSE 操作码提供更多的寄存器和一致性(值始终保持 32 位或 64 位大小),而 FPU 尽可能使用 80 位中间值。因此,您之前很可能从这种改进的中间精度中受益。(请注意,额外的精度可能会导致不一致的结果,例如 x == y 但 cos(x) != cos (y) 取决于计算发生的距离!)

您可以尝试将 -mfpmath=387 用于您的 64 位版本,因为您正在使用 gcc 进行编译,并查看您的结果是否与您的 32 位结果匹配以帮助缩小范围。

于 2009-07-02T19:24:20.163 回答
8

浮点数和双精度数在 32 位和 64 位代码之间的行为并没有本质上的不同,但它们经常这样做。您的问题的答案将取决于平台和编译器,因此您需要说明您要从哪个平台移植以及要移植到哪个平台。

在 intel x86 平台上,32 位代码通常使用 x87 协处理器指令集和浮点寄存器堆栈以获得最大兼容性,而在 amb64/x86_64 平台上,通常使用 SSE* 指令和 xmm* 寄存器来代替。它们具有不同的精度特性。

帖子编辑:

给定您的平台,您可能需要考虑在 x86_64 构建上尝试 -mfpmath=387(i386 gcc 的默认值),看看这是否解释了不同的结果。您可能还想查看所有 -fmath-* 编译器开关的设置,以确保它们与您在两个构建中想要的匹配。

于 2009-07-02T19:24:05.140 回答
3

就像其他人所说的那样,您没有提供足够的信息来准确说明发生了什么。但在一般意义上,您似乎一直在指望某种您不应该指望的浮点行为。

100 次中有 99 次出现问题是您在某处比较两个浮点数是否相等。

如果问题仅仅是您得到的答案略有不同,那么您需要意识到没有一个是“正确的”——无论您使用哪种架构,都会发生某种舍入。这是一个理解计算中的有效数字的问题,并意识到您提出的任何值在一定程度上都是近似值。

于 2009-07-02T19:31:32.927 回答
3

x87 FPU 的 80 位内部寄存器导致其浮点结果与其他内部使用 64 位的 FPU(如 x86_64)略有不同。除非您不介意通过将内容刷新到内存或执行其他“strictfp”技巧来大幅降低性能,否则您将在这些处理器之间获得不同的结果。

另请参阅: 截断时的浮点舍入

并且: http ://docs.sun.com/source/806-3568/ncg_goldberg.html

于 2009-07-02T19:54:01.773 回答
2

在 x64 上,使用 SSE2 指令集,而在 32 位应用程序中,x87 FPU 通常是默认设置。

后者在内部以 80 位格式存储所有浮点值。后者使用普通的 32 位 IEEE 浮点数。

除此之外,重要的一点是你不应该依赖你的浮点数学在架构中是相同的

即使您在两台机器上都使用 32 位版本,也不能保证 Intel 和 AMD 会产生相同的结果。当然,当其中一个运行 64 位构建时,您只会增加更多不确定性。

依赖浮点运算的精确结果几乎总是一个错误。

在 32 位版本上启用 SSE2 也是一个好的开始,但同样,不要对浮点代码做出假设。总是有精度损失,假设这种损失是可预测的,或者它可以在 CPU 或不同版本之间重现是一个坏主意。

于 2009-07-02T19:59:43.493 回答
0

gnu 编译器有很多与浮点数相关的编译器选项,在某些情况下可能会导致计算中断。只需在此页面中搜索“浮动”一词,您就会找到它们。

于 2009-07-02T19:28:08.377 回答
0

真的很难控制很多这样的东西。

首先,C 标准通常要求对浮点数的操作在“双倍空间”中完成并转换回浮点数。

英特尔处理器在用于许多此类操作的寄存器中具有 80 位精度,然后在将其存储到主存储器时将其降至 64 位。这意味着变量的值可能会无缘无故地改变。

如果你真的很在意,你可以使用像 GnuMP 这样的东西,而且我相信还有其他库可以保证一致的结果。大多数情况下,生成的错误/抖动量低于您需要的实际分辨率。

于 2009-07-02T19:56:59.007 回答
0

真正难以获得的部分是两组结果都是正确的。将这些变化描述为“不同”是不公平的。也许对旧结果的情感依恋增加了……但没有数学理由更喜欢 32 位结果而不是 64 位结果。

您是否考虑过更改为该应用程序使用定点数学?定点数学不仅在芯片、编译器和库的变化中保持稳定,而且在许多情况下它也比浮点数学更快。

作为快速测试,将二进制文件从 32 位系统移动到 64 位系统并运行它。然后在 64 位系统上将应用程序重建为 32 位二进制文​​件,然后运行它。这可能有助于确定哪些变化实际上产生了不同的行为。

于 2009-07-02T20:17:58.153 回答
0

正如已经提到的,只要它们都是正确的,不同应该不是问题。理想情况下,您应该对这类事情进行单元测试(纯计算通常属于相对容易测试的阵营)。

基本上不可能保证跨 CPU 和工具链的结果相同(一个编译器标志已经可以改变很多),并且已经很难保持一致。设计健壮的浮点代码是一项艰巨的任务,但幸运的是,在许多情况下,精度不是问题。

于 2009-07-03T02:17:08.990 回答
0

需要注意的一件主要事情是 C 语言最初指定计算

float a=b+c+d;

将 b、c 和 d 转换为最长的可用浮点类型(恰好是 type double),将它们相加,然后将结果转换为float. 这样的语义对编译器来说很简单,对程序员有帮助,但有一点困难:存储数字的最有效格式与执行计算的最有效格式不同。在没有浮点硬件的机器上,对存储为不必要归一化的 64 位尾数和单独存储的 15 位指数和符号的值执行计算,然后对存储为 64 位的值进行运算会更快。少量double必须在每次操作之前解包,然后规范化并在之后重新打包(即使只是为下一次操作立即解包)。让机器以较长格式保存中间结果,提高了速度和准确性;ANSI C 允许使用 type 进行此操作long double

不幸的是,ANSI C 未能提供一种方法,通过该方法,变量参数函数可以指示它们是否希望将所有浮点值转换为long double、全部转换为double,或者将floatdouble传递为doublelong doubleas long double。如果存在这样的设施,那么编写不必区分值doublelong double值的代码会很容易。不幸的是,缺少这样的功能意味着在系统上,double并且long double是不同类型的代码确实必须关心区别,而在它们不是的系统上则不需要。这反过来意味着在类型相同的系统上编写的大量代码会在类型不同的系统上中断。编译器供应商决定最简单的解决方法是简单地使其long double成为同义词,double而不提供任何可以准确保存中间计算的类型。

由于以不可表示的类型执行中间计算是不好的,因此有些人认为合乎逻辑的事情是将计算float作为 type 执行float。虽然在某些硬件平台上这可能比使用 type 更快double,但它通常会对准确性产生不良后果。考虑:

float triangleArea(float a, float b, float c)
{
  long double s = (a+b+c)/2.0;
  return sqrt((s-a)*(s-b)*(s-c)*c);
}

在使用 执行中间计算的系统上long double,这将产生良好的准确性。在执行中间计算的系统上,即使 a、b 和 c 都可以精确表示float,这也可能产生可怕的准确性。例如,如果a和b是16777215.0f,c是4.0f,则s应该是16777217.0,但是如果a、b和c之和计算为float,则为1677216.0;这将产生一个小于正确值一半的区域。如果 a 和 c 是 16777215.0f 而 b 是 4.0f(相同的数字;不同的顺序),那么s将被计算为 16777218.0,产生的面积太大了 50%

如果您的计算在 x86 上产生了良好的结果(许多编译器急切地升级为 80 位类型,即使它们无益地使其对程序员不可用)但在 x64 上的结果很糟糕,我猜您可能会有类似的计算以上需要以比操作数或最终结果更高的精度执行中间步骤。将上述方法的第一行更改为:

  long double s = ((long double)a+b+c)/2.0;

将强制以更高精度完成中间计算,而不是以低精度执行计算,然后将不准确的结果存储到更高精度的变量中。

于 2015-04-03T23:13:28.777 回答