记录一个FPU的猥琐Bug

貌似n多人遇到过,知道问题的原因之后用合适的关键字可以搜出很多帖子。核心问题归结为:float单精度浮点数有效位数不足,而FPU控制寄存器CTRL的bit8和bit9被置为0时,使得FPU被强制工作在单精度(24bit)模式,导致结果出错。

起因是一个朴素的string转浮点数函数,由于输入数据的范围被设定在0~1,000,000且包含两位小数,所以最初的版本使用float储存结果就会导致精度不足。发现问题之后,第一反应就是采用double来储存,理论上不会出错了。结果神奇的事情发生了,改成double之后精度还是不足。

接下来省略掉各种无用处的猜想,再去掉其他的逻辑,这个函数其实就这么一行:

double foo( int input ) { return (double)input / 100; }

简化的症状就是,我调用foo(100,000,000)期待返回1,000,000.01,实际返回的是1,000,000.00

当我尝试在程序最开始调用这个函数,例如放到main的最前面,结果是正确的。如果初始化完其他模块之后,结果就不对了。这样重现n次之后,脑子顿时就不转了,居然存在同一个函数同样的输入但输出不同!我确定这个函数木有inline,就算有啥优化,跑起来的汇编指令也完全一样的说。所以开始考虑是不是thread context有什么不同导致……其实这时候如果对FPU的工作机制比较熟,肯定马上就可以想到。可惜我早就把这种东西还给课本了,只好傻乎乎的肉眼看寄存器有什么问题。

最初想复杂了,脑子一抽想到了以前看过云风一篇帖子,模糊记得是什么东西导致浮点运算出错。搜出来一看,原来人家说的是编译器的问题,比我这个严重的多。不过因为有这么个模糊的印象,又通读了一遍这个帖子,接下来就放在研究FPU那坨诡异的寄存器上面了。

对比发现那条浮点除法指令FDIVR调用过后,结果正确的那次STAT寄存器有所变化,bit8置为了1。google之了解到bit8是如果为1,一切正常,如果为0,代表运算精度不足,FPU对结果做了roundup,大概就是这个意思吧。看来确实是精度问题,但double肯定是完全能存下我要的结果的说……疑问没有消除,继续肉眼看半天,又发现二者的CTRL寄存器也有所不同:正确的那次是0x27F,错误的那次是0x07F。马上google之,果然查到CTRL的bit8和bit9是用来控制运算精度的,(CTRL&0x300)之后,默认状态下应该为0x200,表示双精度(53bit),如果是0x000则表示单精度(24bit)。

好吧,如此看来,第一次调用FPU的时候,CTRL还是用双精度,第二次就单精度了,怪不得出错。问题是这个东东怎么会改变呢?继续google,换了几次关键字终于找到,居然是D3D的CreateDevice干的。DX的文档《Top Issues for Windows Titles》一节中有说,如果没有设置D3DCREATE_FPU_PRESERVE,则FPU被强制设成单精度。更加诡异的是这个东西的影响是per-thread,其他的thread如果没有碰过FPU的CTRL,那么还是默认的精度。为何D3D要做这种处理?彰显它的浮点运算能力?我不是很理解。再次回到程序中验证,的确是图像模块的Init被调用之后,CTRL就奇迹般的从0x27F变成了0x07F了。

解决方案就不说 了,总之问题原因探究到此为止。果然,就如同江湖上传闻的那样,使用浮点数存在各种诡异陷阱。这次悲剧的debug之后,最令人欣慰的一点是让我对float这个该死的单精度浮点数更加敏感……虽说丫是32bit的,其实也就7位有效数字,唉……

One thought on “记录一个FPU的猥琐Bug”

Leave a Reply

Your email address will not be published. Required fields are marked *