题目
0.1 + 0.2 是否等于 0.3 ?
解答
首先直接通过浏览器开发者模式打开控制台看一下结果。
其实看到这个题目应该就可以猜到肯定不可能是 0.3,否则就不会出这个题目了。这个题目涉及到数学运算中的浮点运算。
0.1 转二进制
0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …
左右两边一直乘以2,将小数与正数分开,得到以下结果。
0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)
…
这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……
0.2 转二进制
0.2 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …
左右两边一直乘以2,将小数与正数分开,得到以下结果。
0 + 0.4 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.8 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
1 + 0.6 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 1)
1 + 0.2 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
0 + 0.4 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 0)
0 + 0.8 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
1 + 0.6 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 1)
1 + 0.2 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)
…
0.2 用二进制表示就是 0.00110011001100110……
浮点数
-
整数型存储整数,浮点型存储小数。显示浮点数的方法有两种,单精度和双精度。单精度用 32 位表示,双精度用 64 位表示。JavaScript 是遵循国际 IEEE 754 标准,将数字存储为双精度浮点数,也就是用 64 位表示。
-
最大数和最小数一般用科学计数法来表示。比如 0.000045 可以表示为 0.45 * 10^4。某市有 10000000 人口可以表示为 1 * 10 ^7。科学记数法主要是为了书写方便。
-
对于二进制也是一样,以 0.1 的二进制 0.00011001100110011…… (后面有0.1转二进制的过程)这个数来说:
可以表示为:1 * 2^-4 * 1.1001100110011……
二进制科学计数法公式为:V = (-1)^S (1 + Fraction) 2^E -
所有浮点数都可以用以上公式来表示,所以我们只需要把变化的 S,Fraction 以及 E 存储即可。以 64 位存储为例,其中用 1 位存储 S(sign,存在位 63 上,表示符号位,取 0 表示正数,1表示负数)。用 11 位存储 E+bias,存在位 52-62 上。用 52 位存储 Fraction,存在位 0-51 上。
综上所述,上文 1 * 2^-4 * 1.1001100110011…… 例子中 ,
Sign(该数为正数)为 0。
Fraction(小数部分)为 1001100110011……。
Exponent(指数)为 -4。
bias (这里是为了辅助存储 E,因为要存11位,如果只是正数的话是 2^11 -1 = 2047,范围是 0 - 2046。但是可能存在负数,所以取值范围为 -1023-1023,为了不存负数,存储的时候需要加 1023( 2^(11-1)-1=1023),取值的时候再减去1023)。所以 E+bias = -4+1023 = 1019。而 1019 的二进制为 1111111011。
所以 0.1 用 64 位二进制表示如下:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理,0.2 用 64 位二进制表示如下:
0 01111111100 1001100110011001100110011001100110011001100110011010
浮点数运算
浮点数运算有五个步骤:对阶、尾数运算、规格化、舍入处理、溢出判断。
-
对阶就是把阶码对齐,其目的是为了使两个浮点数的尾数进行加减运算。比如 0.1 的二进制科学记数法是
1.1001100110011…… * 2^-4
,阶码就是 -4。而 0.2 的二进制科学记数法是1.10011001100110...* 2^-3
,阶码就是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3 -
尾数运算
0.1100110011001100110011001100110011001100110011001101
+
1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111 -
规格化
将这个结果处理一下,即结果规格化,变成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2 -
舍入处理
括号里的 1 是计算后这个 1 超出了范围,也就是超出了 52 位,所以要舍弃。四舍五入对应到二进制中,就是 0 舍 1 入,因为要把括号里的 1 丢了,这里会进 1,结果变成 1.0011001100110011001100110011001100110011001100110100 * 2^-2PS:这里不涉及溢出判断。
所以最终的结果存成 64 位就是
0 01111111101 0011001100110011001100110011001100110011001100110100
将它转换为 10 进制数就得到 0.30000000000000004440892098500626
因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3
总结
- 小数转二进制可能有些麻烦,需要两边不断乘2取0或1。
- 用科学计数法时,我们只需要存储 S、Fraction、E(E+bais)。
- 用 64 位表示的二进制中,其中1位表示符号位,11位表示指数位(注意这里是 E+bias),52位表示小数位。