简短的问题:
如何在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;
}
答案 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
电话时没有分歧,您也应该逐步完成。
如果您知道Xi
和K
彼此接近,则最好将log(Xi/K)
计算为log1p((Xi-K)/K)
。当Xi
和K
彼此接近时,减法Xi-K
是准确的(没有错误),并且商有更多有用的位(我们已经知道的1和一些它之后的零位消失了。
浮点环境的轻微变化会导致结果发生0.5%的变化,这意味着您的计算对错误非常敏感。这表明,即使您使结果可重现,浮点运算中必然存在的错误也会导致结果不准确。也就是说,最终的错误仍然存在,只是不会通过两种不同的计算方式来引起你的注意。
在您的C ++实现中,unsigned
是四个字节但double
是八个字节。因此,通过将double
别名化为unsigned
来打印编码,省略了一半的位。相反,您应该将指向double
的指针转换为指向const char
的指针并打印sizeof(double)
个字节。