blog icon indicating copy to clipboard operation
blog copied to clipboard

基础系列 - 奇怪的 0.1 + 0.2 与 IEEE 754

Open HXWfromDJTU opened this issue 4 years ago • 0 comments

IEEE 754 - 维基百科

在计算机的世界中,浮点数的表示范围优先。浮点数只是可以近似的标识一个数而已。与许多其他编程语言不同,JavaScript 并未定义不同类型的数字数据类型,而是始终遵循国际 IEEE 754 标准,将数字存储为双精度浮点数。

  • sign 符号位
  • exponent 指数位
  • mantissa 尾数部分

十进制转二进制补课(敲黑板)

整数部分

用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。

小数部分

用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来。

动手试一下

接下来我们手动尝试一下10.3 + 12.4这个组合,我先帮你在Chrome devtool测试了一下

整数部分

小数部分

整合

整数部分: 10 ==> 1010
小数部分: 0.3 ==> 01 0011 0011 0011 0011 ......
科学计数表示: 1.010010011001100110011... x 103

IEEE 表示法

  • 符号位
    • 0表示正数,1表示负数
    • 这里是 0
  • 指数部分
    • 双精度浮点数这部分一共是11位,也就是基准偏移量是 211 - 1 - 1。科学计数法的幂值是3也就是偏移量为3。这里指数部分就是 基准偏移量 + 偏移量(可能为负喔)
    • 1023 + 3 = 1026,转换为二进制就是 100 0000 0010
  • 尾数部分
    • 尾数部分就直接将科学计数表示法的小数部分迁移过来就行,最多52位,不满的话就用0来补。
    • 0100 1001 1001 1001 1001 1001 1001......

按照Double Float Precision IEEE 754的格式拼起来就是0 100 0000 0010 0100 1001 1001 10001 1001 1001......

结果验证

加法运算

按照上面IEEE 双精度的表示法,两个数字分别如下,然后进行尾数求和

0 100 0000 0010 0100 1001 1001 1001 1001 1001...

0 100 0000 0010 1000 1001 1001 1001 1001 1001...
——————————————————————————————————————————————————
                1101 0011 0011 0011 0011 0010 

然后进行的是尾数的规格化: 1101 0011 0011 0011 0011 0010 ===> 0110 1001 1001 1001 1001 1001,其实就是把末尾的0给去掉了,前面补0

表示范围

因为mantissa(尾数部分)的固定长度为52位,最多可以表示252 + 1(也即是9007199254740992)个数字,用科学技术法来表示也就是9.007199254740992 x 10 16

在控制台我们可以测试到

(0.1).toPrecision(16)  // "0.1000000000000000"
(0.1).toPrecision(17)  // "0.10000000000000001"
(0.1).toPrecision(18)  // "0.100000000000000006"
(0.1).toPrecision(20)  // "0.10000000000000000555"

(0.2).toPrecision(16)  // "0.2000000000000000"
(0.2).toPrecision(17)  // "0.20000000000000001"
(0.2).toPrecision(18)  // "0.200000000000000011"

(0.3).toPrecision(16)  // "0.3000000000000000"
(0.3).toPrecision(17)  // "0.29999999999999999"
(0.3).toPrecision(20)  // "0.29999999999999998890"

所以,我们平时看到的 0.1 并不是只有 0.1,看到的 0.3也不一定够0.3,只是显示精度作怪而已。

toPrecision 与 toFixed
// 以定点表示法或指数表示法表示的一个数值对象的字符串表示,四舍五入到 precision 参数指定的显示数字位数。
numObj.toPrecision(precision)
// 使用定点表示法表示给定数字的字符串。
numObj.toFixed(digits)

其实,precision是指从小数点开始从左往右开始数,第一个不为0的数字开始计数。而fixed是指小数点后开始算的位数。

总结

显而易见,在IEEE体系中,二进制只能够近似地表示某个浮点数数值。比如0.3,使用标准的转换方式,我们永远也达不到终止条件---小数位为0。在存储空间有限的情况下,无尽的循环到达边界时,就不得不进行类似十进制的四舍五入进位了。

实战

在实际项目中,直接使用floatdouble进行金额数值运算也就有可能出现未知的情况,在许多语言中会有Decimal数据类型(例如Python)。而在JavaScript中则是推荐使用Decimal.js

参考资料

[1] Double (IEEE754 Double precision 64-bit)

[2] 消灭烦人的IEEE754困惑--知识梳理

[3] 为什么 0.1 + 0.2 = 0.300000004 - 动力节点

HXWfromDJTU avatar Jul 26 '20 16:07 HXWfromDJTU