2

简短的问题:

在 FPU 上设置 _EM_INVALID 异常标志如何导致不同的值?

长问题:

在我们的项目中,我们在 Release 构建中关闭了浮点异常,但在 Debug 构建中使用 _controlfp_s() 打开了 ZERODIVIDE、INVALID 和 OVERFLOW。这是为了捕获错误(如果存在)。

但是,我们也希望数值计算的结果(包括优化算法、矩阵求逆、蒙特卡洛和各种事情)在 Debug 和 Release 构建之间保持一致,以使调试更容易。

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

这会传播到结果值的 0.5% 差异。

下面的代码将在将其添加到 Visual Studio 2005、Windows XP 中的新解决方案并在调试配置中编译时给出显示的程序输出。(Release 会给出不同的输出,但那是因为优化器重用了第一次调用 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;
}
4

1 回答 1

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在任何测试之前评估一次,将其存储在 adouble中,并将其foo作为参数传递给。测试log调用是否因浮点状态而异。我怀疑是这样,因为分区操作不太可能有所不同。

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

附带说明:

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

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

它出现在您的 C++ 实现中,它unsigned是 4 个字节,但却double是 8 个字节。double因此,通过将编码 a 别名为a 来打印编码 a会unsigned省略一半的位。相反,您应该将指向 的指针转换为指向并打印字节double的指针。const charsizeof(double)

于 2013-08-07T14:11:03.277 回答