GNU C ++中程序的奇怪行为,使用浮点数

时间:2014-11-12 11:06:25

标签: c++ gcc floating-point double compiler-optimization

看看这个节目:

#include <iostream>
#include <cmath>

using namespace std;

typedef pair<int, int> coords;

double dist(coords a, coords b)
{
    return sqrt((a.first - b.first) * (a.first - b.first) +
              (a.second - b.second) * (a.second - b.second));
}

int main()
{
    coords A = make_pair(1, 0);
    coords B = make_pair(0, 1);
    coords C = make_pair(-1, 0);
    coords D = make_pair(0, -1);

    cerr.precision(20);
    cerr << dist(A, B) + dist(C, D) << endl;
    cerr << dist(A, D) + dist(B, C) << endl;

    if(dist(A, B) + dist(C, D) > dist(A, D) + dist(B, C))
    {
        cerr << "*" << endl;
    }
    return 0;
}

函数dist返回两点之间的距离。 A,B,C,D是正方形的角落。

它应该是dist(A,B)== dist(B,C)== dist(C,D)== dist(D,A)== sqrt(2)。

和dist(A,B)+ dist(C,D)== dist(A,D)+ dist(B,C)== 2 * sqrt(2)

我正在使用GNU / Linux,i586,GCC 4.8.2。

我编译这个程序并运行:

$ g++ test.cpp ; ./a.out 
2.8284271247461902909
2.8284271247461902909
*

我们看到,该程序输出dist(A,B)+ dist(C,D)和dist(A,D)+ dist(B,C)的相等值,但条件dist(A,B)+ dist (C,D)&gt; dist(A,D)+ dist(B,C)是真的!

当我使用-O2编译时,它看起来没问题:

$ g++ test.cpp -O2 ; ./a.out 
2.8284271247461902909
2.8284271247461902909

我认为,这是一个gcc-bug,它与浮点运算精度没有直接关系,因为在这种情况下值dist(A,B)+ dist(C,D)和dist(A,D) )+ dist(B,C)必须相等(每个都是sqrt(2)+ sqrt(2))。

当我更改功能dist时:

double dist(coords a, coords b)
{
    double x = sqrt((a.first - b.first) * (a.first - b.first) + (a.second - b.second) * (a.second - b.second));
    return x;
}

程序运行正常。所以问题不在于数字的浮点表示,而是在gcc代码中。

编辑:

32位编译器的简化示例:

#include <iostream>
#include <cmath>
using namespace std;
int main()
{
    if (sqrt(2) != sqrt(2))
    {
        cout << "Unequal" << endl;
    }
    else
    {
        cout << "Equal" << endl;
    }
    return 0;
}

无优化运行:

$ g++ test.cpp ; ./a.out 
Unequal

使用-ffloat-store运行:

$ g++ test.cpp -ffloat-store ; ./a.out 
Equal

解决方案:

可能是GCC#323中的“不是错误”:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323

使用-ffloat-store进行编译可以解决问题。

1 个答案:

答案 0 :(得分:4)

这个看似奇怪的行为是由于旧的x87浮点单元的工作方式:它使用80位long double类型,其64位精度作为其寄存器格式,而临时double s是64位长,53位精度。会发生什么情况是编译器将sqrt(2)结果中的一个溢出到内存中(因为sqrt返回double,这将转换为该类型的53位有效数字),以便FP寄存器-stack对于sqrt(2)的下一次调用很清楚。然后,它将从内存加载的53位精度值与从另一个sqrt(2)调用返回的未经接收的64位精度值进行比较,它们之间的差别不同,因为它们的舍入方式不同,正如你从这个汇编程序输出中看到的那样(注释我的第二个代码片段,2s为清晰度改为2.0s,-Wall -O0 -m32 -mfpmath=387 -march=i586 -fno-builtin改为Godbolt上的编译标志):

main:
    # Prologue
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $20, %esp
    # Argument push (2.0)
    subl    $8, %esp
    movl    $0, %eax
    movl    $1073741824, %edx
    pushl   %edx
    pushl   %eax
    # sqrt(2.0)
    call    sqrt
    # Return value spill
    addl    $16, %esp
    fstpl   -16(%ebp)
    # Argument push (2.0)
    subl    $8, %esp
    movl    $0, %eax
    movl    $1073741824, %edx
    pushl   %edx
    pushl   %eax
    # sqrt(2.0)
    call    sqrt
    addl    $16, %esp
    # Comparison -- see how one arg is loaded from a spill slot while the other is
    # coming from the ST(0) i387 register
    fldl    -16(%ebp)
    fucompp
    fnstsw  %ax
    # Status word interpretation
    andb    $69, %ah
    xorb    $64, %ah
    setne   %al
    testb   %al, %al
    # The branch was here, but it and the code below was snipped for brevity's sake

结论:x87是怪人。 -mfpmath=sse是此行为的最终解决方案 - 它将使GCC将FLT_EVAL_METHOD定义为0,因为SSE(2)浮点支持仅为单/双。 -ffloat-store开关也适用于此程序,但不建议将其作为通用解决方法 - 它会因为额外的溢出/填充而使程序变慢,并且在所有情况下都不起作用。当然,使用64位CPU / OS /编译器组合也可以解决这个问题,因为x86-64 ABI默认使用SSE2进行浮点运算。