zoukankan      html  css  js  c++  java
  • FPU同步(翻译)

    本篇翻译的原英文在:http://mauve.mizuumi.net/2013/06/16/desyncs-and-fpu-synchronization/#more-725(可能要FQ)

        如果你曾经处理过跨系统的同步问题的话,那么碰到不同步的问题也就见怪不怪了。其中的一些是比较容易处理的,但是另外一些则可能让你连续几周抓狂。

        当然,在这些问题中,最令人头疼的就是浮点数了。即使是0.000000001的误差也会很快地累积膨胀,导致更大的问题。传统的做法是在与逻辑相关的部分不要使用浮点数,但这种做法并不是任何场景都可行的。如果你了解在不同场合下浮点数带来的陷阱,那么使用浮点并保持完全的同步也是可能的。

        先快速描述下浮点数的工作方式。以单精度浮点数为例,存储由三部分组成,1bit的符号位,8bit的指数部分,23bit的尾数部分。实际的数据大小为:sign*value*pow(2,exponent)。这意味着如果你超出了value所限定的大小范围((2^24)-1,即16777215),你将会丧失一定精度,而只会保留较大量级的数据。与此相较,双精度浮点数共64位,包含11位的指数部分和52位的尾数部分,有更大的容差空间。

        这里需要注意的是数据不能被2的次幂整除的情况,这种情况下只能用无理数表示。0.1f+0.2f并不精确等于0.3f,实际上0.1f+0.2f的计算结果为0.300000004470(译注:可以做下测试,大同小异),因为其中任一者都不能被2的次幂整除。出于这个原因,千万不要在在对浮点数做了数学运算后又比较相等性(译注:我在LUA交互命令行上测试时:print(0.3==0.3) => true; print(0.1+0.2==0.3) => false; LUA上是双精度浮点)。

        当你意识到浮点常量在编译时会被编译器转化成二进制形式,而且可能还会有轻微的差别时,事情就更有趣了。不过至目前为止我还没有碰到过这种情况,即使碰到了,我也不会感到有啥惊讶的。

        浮点内部存储(INTERNAL FLOAT STORAGE)

        这是处理不同步时要考虑的第一个点。内部存储的形式并不是将数据加载进寄存器那样。X86平台的系统使用了基于栈的格式来表示要被处理的操作指令,当你把数据从内存加载入寄存器时它会按照当前的内部精度模式来运行,而不管它原先的模式应该是什么样子的。正常来说这是没有什么害处的,而且还会有那么一丁点的精准度提升,但是由于内部存储的类型在不同平台上是未定义的,便成为不可回避的问题。

        幸运的是,补救的措施相对简单:X86处理器可以设定精度模式,定义被处理数据的限制范围。这点可以通过汇编指令FLDCW来做到,而且Windows上面还有_controlfp/_control78函数可用。当你需要FPU同步时一定要记得做一步,而且保证它不会被取消掉。在你的逻辑处理流程之前运行FINIT和FLDCW指令不会影响到程序的正确运行。

        注意很多高级语言比如C#,是不允许你设置精度模式的,因为其是平台无关的。在这种情况下,你就不能指望浮点数在不同平台上有完全相同的表现了。    

        不是所有事情都是标准化的(NOT EVERYTHING IS STANDARDIZED)

        下一个问题是虽然IEEE754浮点标准规定了一系列的流程,但是有相当部分的常用库函数是没有定义的。通常对于结果应该是什么样子都会有一些建议,但是没有要求严格遵循。需要特别留意的是有一些超验函数(transcendental functions),或者是任何与三角学有关的函数,比如计算Sine和Cosine。

        这意味着如果你需要一致性的话,那就不能随心所欲地使用数学库中的三角函数了。不同平台上的舍入差异带来的错误总是不可避免的,而且不管你把精度扩大多少,都不能把你从微小误差的传播放大的泥淖中拯救出来。考虑这样一种情况,一个在特定方向上的舍入边界的值(a value that exists right on the boundary of being rounded in a specific direction),在不同的处理器上可能会被区别对待。没有标准的严格说明,没有人能告诉你结果会是什么样。最佳答案是,由你自己定制sin/cos的变种实现来保持一致性,也许会慢一点,但毕竟是可靠的。

        说到舍入不得不提的是,fabs是没有任何问题的。这的确令人惊讶---开平方根的计算有明确定义的舍入标准,因此可以放心使用。

        编译器太聪明(COMPILERS TOO SMART FOR YOUR OWN GOOD)

        现代处理器和编译器非常智能高效,但这也意味着如果开发者不太注意的话,很容易忽略掉它们一些太过聪明的做法。

        举个例子,一个特别的优化是把几个简单的操作合并成一个复杂的指令,在当前场合反而带来了麻烦。比如,Multiply-Accumulate operation会把数学运算构造:a=b+(c*d)转化成一条指令。这种混合的作法只有一个舍入步骤,与把这些运算拆开单独来算是不同的。这个就不好了。你最好把所有这种类似于此的操作全部禁用掉。其它类似于此的"智能"优化依然是有的,比如把a*(b+1)转换成(a*b)+a,虽然减少了代码量,但是却产生了不同的计算结果。从debug版本转到release版本时,优化器可能会比你想象得要更智能,所以一定要小心谨慎。

        VC++有一个fp:strict math mode可以禁用上述优化,GCC如果不打开-ffast-math选项那也一切OK。尽管如此,留意一下输出的汇编代码依然是值得的。

        第三方库以及环境因素(THIRD PARTY LIBRARIES AND ENVIRONMENTAL ISSUES)

        是时候把这些东西从你的应用程序中整个儿地清理出去了,甚至包括你所依赖的方方面面。嵌入式脚本语言,比如Lua,非常频繁地把浮点数作为它们的默认数据类型。你需要尽可能地控制这部分的使用。像上述提到的C#,作为在设计上平台无关的语言,是不会让你直接控制这些的(不清楚JAVA如何,但我估计也是这样的)。

        最后,最好的办法也许是在业务逻辑中不要使用浮点数,只使用整数或是定点数。只是把简单地做做数值乘法而不考虑乘/除的话,用整数模拟高精度数是完美的做法。但是记住你依然摆脱不了精度限制!

        不管是以怎样的方式,真诚希望这篇文章能帮助到那些被同样问题所困的人。

  • 相关阅读:
    BeanFactory 工厂模式
    中小型企业架构
    数据状态图
    好文章
    leetcode 最受欢迎的91道题目
    windows下安装mysql8并修改密码
    leetcode 1049 Last Stone Weight II(最后一块石头的重量 II)
    leetcode 910. Smallest Range II
    leetcode 908. Smallest Range I
    leetcode 900. RLE Iterator
  • 原文地址:https://www.cnblogs.com/Jackie-Snow/p/6554181.html
Copyright © 2011-2022 走看看