根据浮点异常标志设置不同的值

时间:2013-08-07 13:25:59

标签: visual-c++ exception floating-point ieee-754 fpu

简短的问题:

如何在FPU上设置_EM_INVALID异常标志会产生不同的值?

很长的问题:

在我们的项目中,我们在Release版本中关闭了浮点异常,但在Debug版本中使用_controlfp_s()启用了ZERODIVIDE,INVALID和OVERFLOW。这是为了捕捉错误,如果他们在那里。

然而,我们还希望数值计算的结果(包括优化算法,矩阵求逆,蒙特卡罗和各种事物)在Debug和Release构建之间保持一致,以便更容易调试。

我希望FPU上的异常标志的设置不应该影响计算值 - 只是抛出异常。但是在通过我们的计算向后工作后,我可以隔离下面的代码示例,该示例显示调用log()函数时最后一位有差异。

这会导致结果值的差异达到0.5%。

以下代码在将其添加到Visual Studio 2005,Windows XP中的新解决方案并在Debug配置中编译时,将显示所显示的程序输出。 (释放将提供不同的输出,但这是因为优化器重用第一次调用log()时的结果。)

我希望有人可以对此有所了解。感谢。

/*
Program output:

Xi, 3893f76f, 7.4555176582633598
K,  c0a682c7, 7.44466687218

Untouched
x,  da8caea1, 0.0014564635732296288

Invalid exception on
x,  da8caea2, 0.001456463573229629

Invalid exception off
x,  da8caea1, 0.0014564635732296288
*/

#include <float.h>
#include <math.h>
#include <limits>
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
    unsigned uMaskOld  = 0;
    errno_t err;

    cout << std::setprecision (numeric_limits<double>::digits10 + 2);

    double Xi = 7.4555176582633598;
    double K  = 7.44466687218;
    double x;

    cout << "Xi, " << hex << setw(8) << setfill('0') << *(unsigned*)(&Xi) << ", " << dec << Xi << endl; 
    cout << "K,  " << hex << setw(8) << setfill('0') << *(unsigned*)(&K) << ", " << dec << K << endl; 
    cout << endl;

    cout << "Untouched" << endl;
    x = log(Xi/K);
    cout << "x,  " << hex << setw(8) << setfill('0') << *(unsigned*)(&x) << ", " << dec << x << endl; 
    cout << endl;

    cout << "Invalid exception on" << endl;

    ::_clearfp();
    err = ::_controlfp_s(&uMaskOld, 0, _EM_INVALID);

    x = log(Xi/K);
    cout << "x,  " << hex << setw(8) << setfill('0') << *(unsigned*)(&x) << ", " << dec << x << endl; 
    cout << endl;

    cout << "Invalid exception off" << endl;

    ::_clearfp();
    err = ::_controlfp_s(&uMaskOld, _EM_INVALID, _EM_INVALID);

    x = log(Xi/K);
    cout << "x,  " << hex << setw(8) << setfill('0') << *(unsigned*)(&x) << ", " << dec << x << endl; 
    cout << endl;

    return 0;
}

1 个答案:

答案 0 :(得分:2)

这不是一个完整的答案,但是评论太长了。

我建议您隔离执行可疑计算的代码并将其放在子例程中,最好是在单独编译的源模块中。类似的东西:

void foo(void)
{
    double Xi = 7.4555176582633598;
    double K  = 7.44466687218;
    double x;
    x = log(Xi/K);
    …Insert output statements here…
}

然后你会用不同的设置调用例程:

cout << "Untouched:\n";
foo();

cout << "Invalid exception on:\n";
…Change FP state…
foo();

这保证了在每种情况下执行相同的指令,消除了编译器由于某种原因为每个序列生成单独代码的可能性。你编译代码的方式,我怀疑编译器可能在一种情况下使用80位运算而在另一种情况下使用64位运算,或者可能一般使用80位算术但是将一些结果转换为64位在一个案例中但不是另一个案例

完成后,您可以进一步分区和隔离代码。例如,尝试在任何测试之前评估Xi/K一次,将其存储在double中,并将其作为参数传递给foo。测试log调用是否因浮点状态而异。我怀疑是这种情况,因为除法操作不太可能不同。

以这种方式隔离代码的另一个好处是,您可以在调试器中单步执行它,以准确查看行为偏离的位置。您可以逐步执行它,一次一条指令,在两个窗口中同时使用不同的浮点状态,并检查每一步的结果,以确切了解分歧的位置。如果到达log电话时没有分歧,您也应该逐步完成。

附带说明:

如果您知道XiK彼此接近,则最好将log(Xi/K)计算为log1p((Xi-K)/K)。当XiK彼此接近时,减法Xi-K是准确的(没有错误),并且商有更多有用的位(我们已经知道的1和一些它之后的零位消失了。

浮点环境的轻微变化会导致结果发生0.5%的变化,这意味着您的计算对错误非常敏感。这表明,即使您使结果可重现,浮点运算中必然存在的错误也会导致结果不准确。也就是说,最终的错误仍然存​​在,只是不会通过两种不同的计算方式来引起你的注意。

在您的C ++实现中,unsigned是四个字节但double是八个字节。因此,通过将double别名化为unsigned来打印编码,省略了一半的位。相反,您应该将指向double的指针转换为指向const char的指针并打印sizeof(double)个字节。