2012-10-19

一个关于浮点运算的坑


这两天遇到一个很奇怪的问题,后来通过排除法终于把故障点缩小到一句代码,但还是很奇怪:

某个同事以前用VC写了一个DLL用于提供某类通用计算,相当于一个计算模块,由我这边写BCB程序来调用。不过在这次的问题中发现,一旦线程调用过pow()这个C库函数来计算过一个非整数指数的幂,那么这个计算模块接下来同样的参数再次用就会得出不一样的结果。调用过pow()之前是一种结果,调用之后是另一种结果,现象相当稳定。

诡异就诡异在pow()的返回值既没有错,也没有被采用过。比如仅仅一句:
pow(2.0, 3.1);
函数返回值根本没有用来干过任何事情,相当于直接丢掉了。那么按道理来说这行代码应该不会对之前或之后的代码造成任何影响。但它确确实实影响了。
然而指数如果是整数,就不会,比如:
pow(2.0, 3.0);

那个写DLL的同事对此也是一头雾水。怀疑点一度被放在VC/BCB身上,因为它们的CRT不一样。但因为正好有一个测试工具,稍加改造便可以针对计算过程输出详细的结果报表,因此用来比较了一下,发现了问题:出问题的地方,有两个本来应该用来比较的double型输入数据正好是相等的。在pow()调用之前,它们的确被判断为相等。但调用pow()之后,计算结果显示它们被判断为不相等了。

这种事情对于常写浮点运算相关代码的程序员而言是很容易引起警惕的。浮点数不能直接用==之类来比较,必须去判断两数相减的绝对值是否小于某个精度。所以将这个测试结果提交给写DLL的同事之后,很快就定位并解决了问题。
但这里我更关注的是以下几个问题:

  1. pow(2.0, 3.0)和pow(2.0, 3.1)的不同,使我相信CRT肯定对前者作了优化。这种事情,想得通,但后果可能是个坑。
  2. 很明显,CRT在pow()被调用之后,处理浮点数时的行为模式改变了。通过观察_statusfp()的返回值,我发现调用前为0,调用后变成了0x20。但这个0x20是一个Undocumented的东西,哪怕是最新的MSDN上也找不到。并且我通过_fpreset()将状态字变回了0,但计算模块仍然会出错,说明应该还有别的东西也被改了。这个坑是不是VC和BCB联合挖的,目前还不知道。
  3. 如果不知道有这个坑,就可能导致一些大麻烦。在特定的代码逻辑中,这个问题很难通过黑盒测试发现。它可能在很长一段时间内都能工作得很好,直到某一次有个用户算了一个指数带小数点的幂,然后……一切就不一样了。这简直就是逻辑炸弹嘛!让我想起了《深渊上的火》里面可怜的蓝荚和绿茎。
  4. MSDN真心不是完全靠得住的。

在本文最后,还要对提供过重要帮助的Libin Yan表示感谢!并感谢所有关注和评论过这个PO的G+网友!

1 条评论: