visual-c++ - 不同的值取决于浮点异常标志设置

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

简短问题:

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

长问题:

在我们的项目中,我们在发布版本中关闭了浮点异常,但在调试版本中使用 _controlfp_s() 打开了 ZERODIVIDE、INVALID 和 OVERFLOW。这是为了捕获存在的错误。

但是,我们还希望调试和发布版本之间的数值计算结果(涉及优化算法、矩阵求逆、蒙特卡罗等)保持一致,以便于调试。

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

这会导致结果值出现 0.5% 的差异。

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

最佳答案

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

我建议您隔离执行有问题计算的代码并将其放入子例程中,最好放在单独编译的源模块中。像这样的东西:

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 是八个字节。因此,通过将编码别名为 unsigned 来打印 double 会省略一半的位。相反,您应该将指向 double 的指针转换为指向 const char 的指针,并打印 sizeof(double) 字节。

关于visual-c++ - 不同的值取决于浮点异常标志设置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18104660/

相关文章:

c++ - 为什么 C++ 中的分号似乎隐式处理错误? [tldr : they don't]

c++ - 如何使用 char* 调用带有 string& 参数的方法?

.net - 用作事件接收器的 CCmdTarget 的正确生命周期管理是什么?

c - 链接错误浮点格式未链接

c# - 使用 Single.Parse() 解析 float

c - 在 C 中如何将浮点值限制为小数点后仅两位?

visual-c++ - Gdiplus 64 位颜色

exception - PostgreSQL 自定义异常?

c++ - 错误内存位置的 Lua 参数

perl - 在 Perl 的 ssh 连接中捕获错误主机名的错误消息(使用 Net::OpenSSH)