【问题标题】:64 bit floating point porting issues64 位浮点移植问题
【发布时间】:2009-07-02 19:17:03
【问题描述】:

我正在将我的应用程序从 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 位依赖性。

【问题讨论】:

  • 平台和编译器在这里相关...
  • 是的,你可以强制它使用 FPU。正如我在下面的回答中指出的,尝试 -mfpmath=387 使其仅在使用 SSE 内在函数时使用 SSE。

标签: c++ c floating-point 64-bit portability


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

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

    后期编辑:

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

    【讨论】:

    • 我如何找到打开了哪些 -fmath- 开关?
    • 默认值都在信息页面中指定。尝试“info gcc”,然后搜索“-fmath”。
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

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

      另请参阅: Floating point rounding when truncating

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

      【讨论】:

        【解决方案5】:

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

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

        除此之外,要强调的重要一点是您不应该依赖于您的浮点数学在架构之间是相同的。

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

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

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

        【讨论】:

          【解决方案6】:

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

          【讨论】:

            【解决方案7】:

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

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

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

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

            【讨论】:

              【解决方案8】:

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

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

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

              【讨论】:

                【解决方案9】:

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

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

                【讨论】:

                  【解决方案10】:

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

                  float a=b+c+d;
                  

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

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

                  由于以不可表示的类型执行中间计算是不好的,一些人认为合乎逻辑的事情是将float 上的计算作为float 类型执行。虽然在某些硬件平台上这可能比使用 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 执行中间计算的系统上,这将产生良好的准确性。在以float 执行中间计算的系统上,即使 a、b 和 c 都可以精确表示,这可能会产生可怕的准确性。例如,如果 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;
                  

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

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-10-06
                    • 1970-01-01
                    • 2020-10-28
                    • 1970-01-01
                    • 2011-03-07
                    • 1970-01-01
                    • 2013-11-30
                    相关资源
                    最近更新 更多