启用优化后会产生不同的浮点结果 - 编译器错误?

时间:2011-09-22 15:44:33

标签: c++ optimization g++ c++-faq

以下代码适用于Visual Studio 2008,包括优化和不优化。但它只适用于没有优化的G ++(O0)。

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

输出应为:

4.5
4.6

但是带有优化的g ++(O1 - O3)将输出:

4.5
4.5

如果我在t之前添加volatile关键字,它会起作用,那么可能会出现某种优化错误吗?

测试g ++ 4.1.2和4.4.4。

以下是关于ideone的结果: http://ideone.com/Rz937

我在g ++上测试的选项很简单:

g++ -O2 round.cpp

更有趣的结果,即使我在Visual Studio 2008上打开/fp:fast选项,结果仍然是正确的。

进一步的问题:

我想知道,我是否应该始终打开-ffloat-store选项?

因为我测试的g ++版本附带CentOS / Red Hat Linux 5和CentOS / Redhat 6

我在这些平台下编译了很多程序,我担心它会在我的程序中引起意外的错误。调查我的所有C ++代码并使用库是否有这样的问题似乎有点困难。有什么建议吗?

是否有人对即使/fp:fast开启的原因感兴趣,Visual Studio 2008仍然可以使用?看起来Visual Studio 2008在这个问题上比g ++更可靠吗?

7 个答案:

答案 0 :(得分:85)

Intel x86处理器内部使用80位扩展精度,而double通常为64位宽。不同的优化级别会影响来自CPU的浮点值保存到内存中的频率,从而将其从80位精度舍入到64位精度。

使用-ffloat-store gcc选项获得具有不同优化级别的相同浮点结果。

或者,使用long double类型,通常在gcc上为80位宽,以避免从80位精度到64位精度。

man gcc说明了一切:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

答案 1 :(得分:10)

  

输出应为:4.5 4.6   如果您具有无限精度,或者您使用的是使用基于十进制而不是基于二进制的浮点表示的设备,那么这就是输出。但是,你不是。大多数计算机使用二进制IEEE浮点标准。

正如Maxim Yegorushkin在他的回答中已经指出的那样,问题的部分是你的计算机内部正在使用80位浮点表示。不过,这只是问题的一部分。该问题的基础是任何数量的n.nn5形式都没有精确的二进制浮点表示。那些极端情况总是不准确的数字。

如果你真的希望你的舍入能够可靠地围绕这些极端情况,你需要一个舍入算法来解决n.n5,n.nn5或n.nnn5等事实(但不是n.5) )总是不准确。找到确定某个输入值是向上​​还是向下舍入的角点大小写,并根据与此角点大小写的比较返回向上舍入或向下舍入的值。而且你需要注意优化编译器不会将找到的极端情况放在扩展精度寄存器中。

有关此类算法,请参阅How does Excel successfully Rounds Floating numbers even though they are imprecise?

或者你可以忍住角落案件有时会错误地绕过这个事实。

答案 2 :(得分:6)

不同的编译器具有不同的优化设置。根据{{​​3}},其中一些更快的优化设置不会维护严格的浮点规则。 Visual Studio具有特定设置/fp:strict/fp:precise/fp:fast,其中/fp:fast违反了可以执行的操作的标准。您可能会发现标志是控制此类设置中的优化的标志。您可能还会在GCC中找到类似的设置,这会改变行为。

如果是这种情况,那么编译器之间唯一不同的是,GCC会在更高优化时默认寻找最快的浮点行为,而Visual Studio不会更高优化级别的浮点行为。因此,它可能不一定是一个真正的错误,但是你不知道你正在打开的选项的预期行为。

答案 3 :(得分:4)

  

对于那些无法重现错误的人:不要取消注释已注释掉的调试stmts,它们会影响结果。

这意味着该问题与调试语句有关。看起来在输出语句中将值加载到寄存器中会导致舍入错误,这就是为什么其他人发现您可以使用-ffloat-store

来解决这个错误
  

进一步的问题:

     

我想知道,我是否应该始终开启-ffloat-store选项?

为了轻率,必须有一个程序员没有打开-ffloat-store的原因,否则该选项将不存在(同样,必须有一些程序员执行开启-ffloat-store)。我不建议总是打开它或总是将其关闭。打开它可以防止一些优化,但关闭它可以实现你所获得的那种行为。

但是,通常情况下,二进制浮点数(如计算机使用)和十进制浮点数(人们熟悉)之间存在some mismatch,并且不匹配可能会导致与获取的相似的行为(要明确的是,这种不匹配导致的行为,但类似的行为可以。问题是,由于你在处理浮点时已经有一些模糊性,我不能说-ffloat-store使它变得更好或更糟。

相反,您可能希望查看other solutions您要解决的问题(不幸的是,Koenig没有指出实际的论文,我真的找不到一个明显的“规范”它的位置,所以我必须把你送到Google)。


如果您没有为输出目的进行舍入,我可能会看std::modf()cmath}和std::numeric_limits<double>::epsilon()limits)。考虑到原来的round()函数,我相信通过调用此函数来替换对std::floor(d + .5)的调用会更干净:

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

我认为这表明了以下改进:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

一个简单的说明:std::numeric_limits<T>::epsilon()被定义为“添加到1的最小数字,它创建的数字不等于1”。您通常需要使用相对epsilon(即,以某种方式缩放epsilon来说明您使用的数字不是“1”)。 d.5std::numeric_limits<double>::epsilon()的总和应该接近1,因此对该添加进行分组意味着std::numeric_limits<double>::epsilon()将与我们正在做的事情大小相当。如果有的话,std::numeric_limits<double>::epsilon()将会过大(当所有三个的总和小于1时)并且可能会导致我们在不应该的时候将某些数字向上舍入。


如今,您应该考虑std::nearbyint()

答案 4 :(得分:1)

如果要编译的x86目标不包含SSE2,则接受的答案是正确的。所有现代x86处理器都支持SSE2,所以如果你可以利用它,你应该:

-mfpmath=sse -msse2 -ffp-contract=off

让我们打破这一点。

-mfpmath=sse -msse2。这通过使用SSE2寄存器执行舍入,这比将每个中间结果存储到存储器快得多。请注意,对于x86-64,这是GCC上的already the default。来自GCC wiki

  

在更现代的支持SSE2的x86处理器上,指定编译器选项-mfpmath=sse -msse2可确保所有浮点和双精度操作都在SSE寄存器中执行并正确舍入。这些选项不会影响ABI,因此应尽可能使用这些选项以获得可预测的数值结果。

-ffp-contract=off。但是,控制舍入并不足以完全匹配。 FMA(融合乘法 - 加法)指令可以改变舍入行为与非融合对应物,因此我们需要禁用它。这是Clang的默认值,而不是GCC。正如this answer所述:

  

FMA只有一个舍入(它有效地保持内部临时乘法结果的无限精度),而ADD + MUL有两个。

通过禁用FMA,我们得到的结果与调试和发布完全匹配,但代价是性能(和准确性)。我们仍然可以利用SSE和AVX的其他性能优势。

答案 5 :(得分:1)

我更深入地研究了这个问题,我可以带来更多的精确度。首先,根据g84在x84_64上的4.45和4.55的精确表示如下(使用libquadmath打印最后一个精度):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

如上所述Maxim,问题是由于FPU寄存器的80位大小。

但是为什么Windows上的问题永远不会发生?在IA-32上,x87 FPU配置为使用53位尾数的内部精度(相当于64位的总大小:double)。对于Linux和Mac OS,使用了64位的默认精度(相当于80位的总大小:long double)。因此,通过更改FPU的控制字(假设指令序列将触发错误),这些不同平台上的问题应该是否可行。该问题已报告为gcc为bug 323(至少阅读评论92!)。

要在Windows上显示尾数精度,可以使用VC ++编译32位:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

并在Linux / Cygwin上:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

请注意,使用gcc,您可以使用-mpc32/64/80设置FPU精度,但在Cygwin中会被忽略。但请记住,它会修改尾数的大小,但不会改变指数的大小,让大门对其他不同的行为开放。

在x86_64架构上,SSE的使用方式如tmandry所述,因此除非您使用-mfpmath=387强制旧的x87 FPU进行FP计算,否则不会出现此问题,或者除非您以32位模式编译使用-m32(您将需要multilib包)。我可以使用不同的标志组合和gcc版本重现Linux上的问题:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

我尝试使用VC ++ / gcc / tcc在Windows或Cygwin上进行一些组合,但这个bug从未出现过。我想生成的指令序列不一样。

最后请注意,使用_Decimal32/64/128来防止这个问题的一种奇特的方法是使用libdfp,但支持真的很少......我花了很多时间才能做到printf与{{1}}!

答案 6 :(得分:0)

就个人而言,我从另一个方面遇到了同样的问题 - 从gcc到VS.在大多数情况下,我认为最好避免优化。唯一值得的是当你处理涉及大型浮点数据数组的数值方法时。即使在拆解后,我也常常被编译器的选择所震惊。通常,使用编译器内在函数或仅自己编写程序集更容易。