简短问题:
在 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
调用时没有出现分歧,您也应该逐步执行该操作。
附带说明:
如果您知道 Xi
和 K
彼此接近,最好将 log(Xi/K)
计算为 log1p((Xi-K)/K)
。当Xi
和K
彼此接近时,减法Xi-K
是精确的(没有误差),并且商更有用位(我们已经知道的 1 和后面的一些零位消失了)。
浮点环境中的微小变化会导致结果发生 0.5% 的变化,这一事实意味着您的计算对错误非常敏感。这表明,即使您使结果可重现,浮点运算中必然存在的错误也会导致结果不准确。也就是说,最终的误差仍然存在,只是不会因为两种不同计算方式的差异而引起您的注意。
在您的 C++ 实现中,unsigned
是四个字节,但 double
是八个字节。因此,通过将编码别名为 unsigned
来打印 double
会省略一半的位。相反,您应该将指向 double
的指针转换为指向 const char
的指针,并打印 sizeof(double)
字节。
关于visual-c++ - 不同的值取决于浮点异常标志设置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18104660/