美文网首页
JS基础-数字-0.1+0.2!=0.3(五)

JS基础-数字-0.1+0.2!=0.3(五)

作者: 火锅伯南克 | 来源:发表于2021-04-11 02:31 被阅读0次

    这一篇真的要说0.1 + 0.2 了。。

    先来一个计算题来热热身吧。

    3.625 x 105 + 3.14 x 102 = ?

    我知道你想怎么做:362500 + 314 = 362814。没错,是这样算的,再来一个。

    3.625 x 1020 + 3.14 x 1016 = ?

    还用刚才的办法吗?还是算了吧,实在太长了,换一种办法:

    = 36250 x 1016 + 3.14 x 1016
    = 36253.14 x 1016
    或者
    = 3.625 x 1020 + 0.000314 x 1020
    = 3.625314 x 1020

    运算法则不用多说了吧,这种方法还是很简单的,IEEE754浮点数也是使用这种办法计算的加法的,准确来说,是计算机中所有种类的浮点数都是这么计算加法的,因为浮点数标准可不只有IEEE一家才有。

    现在借用工具把 0.1 和 0.2 的 64位浮点数格式表示出来。

    工具下载地址:https://github.com/fengjinlovewei/ieee754.git

    图中上面的64位浮点数代表0.1,下面的64位浮点数代表0.2(注意,因为计算时肯定是拿的内存中的值,所以此时的0.1 和 0.2 都是经过存储前舍入处理的了),并且已经列出了加法计算的5个步骤:

    一. 阶码对阶
    二. 尾数相加
    三. 结果规格化
    四. 尾数舍入处理
    五. 溢出判断

    那就按照顺序来一一说明。

    一、阶码对阶

    首先,要把指数位(阶码)调整成一致才能计算加法,但是和开篇的计算题不同的是,这里规定必须是小阶向大阶看齐

    为什么要小阶向大阶看齐??
    因为如果大向小看齐,大阶的尾数必定要左移才能保证和原数值相等,但是左移的值在逻辑电路中是要舍弃的,舍弃的这部分可是计算中的最高值部分。

    还拿 3.625 x 1020 + 3.14 x 1016 举例:
    如果最终结果只能保留小数点前1位,小数点后4位。

    大阶向小阶看齐
    = 36250 x 1016 + 3.14 x 1016
    = 36253.14 x 1016
    根据规则舍弃后得:3.1400 x 1016

    小阶向大阶看齐
    = 3.625 x 1020 + 0.000314 x 1020
    = 3.625314 x 1020
    根据规则舍弃后得:3.6253 x 1020

    你觉得哪种方案更能让人接受呢?肯定是 小阶向大阶看齐 更能让人接受,因为 大阶向小阶看齐 的计算结果和真实结果相差的太离谱了。

    很明显,0.1的阶码比0.2的要小,所以0.1的尾数(小数位)向右移动一位,同时阶码 + 1。

    温馨提示:在工具中,可以长按鼠标左键,拖动阶码部分左右移动,已实现阶码的加减操作,尾数会同步移动,对阶成功点击下一步即可。

    点击下一步后如下所示:

    因为阶码部分暂时已经确定,所以拿出来单独显示。

    二、尾数相加

    这一步比较简单,就是使用加法器把两个值相加,结果如下图所示。

    三、结果规格化

    还记得规格化是什么东西吗,联想一下科学计数法,计算后的尾数需要变成1.xxxx的形式,通过尾数的右移动来实现规格化,整体右移一位,阶码 +1。本案例中需要右移一位即可,所以阶码 +1。

    四、尾数舍入处理

    舍入规则同第四章所说一致,可以返回《JS基础-数字-0.1+0.2!=0.3(四)》查看,当前命中了五取偶。

    由于符合舍入规则五取偶中的 “如果X的值为1,则进1”,所以进1

    这块需要思考一下,舍入进位时是否有可能触发最高位的进位,进而需要再次规格化?
    答案是会,出现这种情况需要再次规格化。

    例如计算 :
    0.99999999999999988 + 9007199254740991


    上:0.99999999999999988,下:9007199254740991 对阶 相加并且规格化后的结果

    到这可以发现,舍入后,最高位会进1,变成10.0000000000..........,所以还会规格化一次,不过在工具中这一步省略了,直接给出了最终结果,不过结果阶码是 + 1的,所以没问题,如下图:



    规格化最多出现2次,不会多出现,因为舍入进位时,进的是尾数最后一位,只有当尾数52位都是1时,才会触发最高位进1,进而触发第二次规格化,但第二次规格化尾数值一定都是0,所以不会出现第3次规格化。

    五、溢出判断

    溢出判断规则:

    1. 如果指数超出 11111111111,则向无穷舍入;
    2. 如果指数小于 0,则向0舍入;

    本案例没有命中溢出,所以不需要处理。

    使用工具计算
    1.7976931348623157e+308 + 1.7976931348623157e+292
    可以直观感受 Infinity 怎么产生的。


    上:1.7976931348623157e+308,下:1.7976931348623157e+292
    对阶
    尾数相加
    根据舍入的六入原则,指数进1,并且规格化,判定为 Infinity,隐藏位为0

    由于本工具只有加法,没有乘除法功能,所以无法演示向下溢出,如果你有什么好点子想完善工具,可以随时联系我。

    所以 0.1+0.2 的和存入内存时的结果为:

    对比一下真正的0.3存入内存时的64位浮点数是什么样的:

    const n = 0.3
    

    细心的同学应该发现了其中的不同,0.1 + 0.2 的最终结果和 0.3 相比,在最后一位多进了一个1。这也就是为什么他们不相等的原因,0.1 + 0.2 已经不是 0.3 了。

    在细说一下,0.1在转化成64位浮点数时,尾数已经进了一个1,0.2在转化成64位浮点数时,尾数也进了一个1,这两个数加在一起,尾数又进了一个1。这么一来,偏移量就大大超出了判定为0.3的界限,最终判定为比0.3大、且与0.3最近的,能用IEEE754-64位浮点编码表示的值, 0.30000000000000004。没办法,赶上了就得认命。

    有同学会说了,为什么不让这两个64位浮点数都显示成0.3呢?问题不就解决了吗?IEEE754的工程师们干什么吃的,这点问题都解决不了。

    首先,每一个IEEE754浮点编码都必须对应一个唯一的10进制的值,这是必须的,因为IEEE754没有规定每一个编码转化成的10进制的值必须是多少,但是却规定,一个IEEE754浮点编码转化成10进制的值不做绝对限制,但当这个10进制的值在转化回IEEE754浮点编码时,必须还能还原成之前的IEEE754浮点编码。

    如果 IEEE754浮点编码00111111110100110011001100110011001100110011001100110011001100110011111111010011001100110011001100110011001100110011001100110100 都代表了0.3,那么当我存储0.3时,要转化成这两个编码的哪一个呢?

    其次,如果如你所想的这么干了,0.3 占用了2个编码,其他的不精准情况是不是也要占用多个编码啊?(比如:0.2 + 0.4 = 0.6000000000000001 等等)这种情况很常见,都这么干, IEEE754浮点编码一共才拥有 18437736874454810627(即,264 - 253 + 3)个值,将会有多少无效的编码掺杂其中。

    最后,如果有个JS开发人员就想存 0.30000000000000004这个值,为啥不让人家存啊, 0.30000000000000004招谁惹谁了,它也是IEEE754 64位浮点编码的合法公民啊。

    无论舍入界限值设置为多少,总会有这个值附近的倒霉蛋赶上。所以我现在只能使用《你不知道的JavaScript (中卷)》19页的最上面的话回复你:

    看来我们能做的只有了解它,并且小心使用。

    结束语:

    请不要以JavaScript的数值精度问题为借口来贬低这门语言,如果你这么做了,只能说明你还不懂爱情。

    作为开发人员,针对小数的存储和运算问题不能怪罪IEEE754,因为误差不是IEEE754特有的,而是二进制浮点数表示方法引起的。怪罪2进制浮点数也不合适,因为计算机是2进制的。计算机也很冤枉,因为科学已经证实自然常数E(约等于2.718281828459045)进制的计算机执行效率最高,与之最近的只能是2和3,2进制计算机的物理原件相比于3进制更容易制造。即便普及的是3进制计算机,10进制小数依然无法完全转化。

    如果你非要找个替罪羔羊泄愤,那只能拿锤子砸自己的手了,因为如果人类生来就只有8个手指头,习惯于使用8进制,转化成2进制小数就不会有转不尽的情况了。

    以上全部,仅代表个人观点。

    相关文章

      网友评论

          本文标题:JS基础-数字-0.1+0.2!=0.3(五)

          本文链接:https://www.haomeiwen.com/subject/ewknhltx.html