前言
此文参照计算机组成原理从底层剖析JavaScript中浮点数参与计算出现精度丢失的原因。JavaScript中Number类型实现遵循IEEE 754标准,使用64位固定长度来表示,即double类型,以下内容都是基于double类型来进行讨论
浮点数计算
以下用例子6.6+1.3来描述计算机中的计算过程
进制转换
将两个数分别转成二进制
6.610 = 110.100110011001...2 // 循环因子为1001
1.310 = 1.0100110011001...2 // 循环因子为1001
固定为64位长度
以64位固定长度存在计算机中
64位符点数的真值表示公式为: x = (-1)S * (1.M) * 2E-1023
按约定尾数最高有效位为1与小数点一起不予存储
真值表示
计算机中为了提高数据的表示精度,当尾数的值不为0时,尾数域的最高有效位应为1,这称为符点数的规格化表示
将十进制按64位规格化后其真值如下表所示
| 原数据 | 真值 |
|---|---|
| 6.610 | x= (-1)0 *(1.1010011001100110011001100110011001100110011001100110)*22 |
| 1.310 | x=(-1)0*(1.0100110011001100110011001100110011001100110011001101)*20 |
64位存储
原数据在计算机中的存储则如下表所示
| 原数据 | 符号位S | 阶码E | 尾数M | 备注 |
|---|---|---|---|---|
| 6.610 | 0 | 10000000001 | 1010011001100110011001100110011001100110011001100110 | 最后一位舍入00 => 0 |
| 1.310 | 0 | 01111111111 | 0100110011001100110011001100110011001100110011001101 | 最后一位舍入01 => 1 |
重新翻译成十进制
对上表中的二进制重新翻译成十进制如下表所示
| 原数据 | 舍入后十进制 |
|---|---|
| 6.610 | 6.5999999999999996447310 |
| 1.310 | 1.3000000000000000444110 |
对阶
由上述表格可知,数值6.6在计算机中的阶码大于数值1.3在计算机中的阶码,因此需调整数值1.3的指数,使其阶码与数值6.6的阶码对齐
| 原数 | 符号位S | 阶码E | 尾数M |
|---|---|---|---|
| 6.610 | 0 | 10000000001 | 1010011001100110011001100110011001100110011001100110 |
| 1.310 | 0 | 10000000001 | 0101001100110011001100110011001100110011001100110011 |
尾数相加
1.1010 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110
+ 0.0101 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
----------------------------------------------------------------------
1.1111 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001
规格化
对计算结果进行规格化,可以得到真值
x = (-1)0*(1.1111 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001)*22
检测上下溢
规格化后阶码为2在1023~-1022之间因此没有出现阶码溢出的情况
结果转十进制
1.1111 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 * 2^2
=> 7.89999999999999946709
计算结果与控制台不一致
在研究为什么浏览器控制台输出的结果为7.8999999999999995而不是7.89999999999999946709之前,我们以x = 0.1为例子进行研究
为什么 x = 0.1 输出为 0.1
0.1在内存中的表示如下表所示
| 原数 | 符号位S | 阶码E | 尾数M |
|---|---|---|---|
| 0.110 | 0 | 0111111 1011 | 1001100110011001100110011001100110011001100110011010 |
当我们使用toPrecision(17)获取0.1的精度,可以得到0.10000000000000001
那么针对0.1为什么控制台不显示0.10000000000000001而显示0.1呢?
因为计算机中许多不同的十进制数共享相同的最接近的近似二进制小数,而在展示上,大多数系统上会选择这些不同的十进制数中最短的来展示。
回到0.1的例子中,其实存在着十进制数0.1、0.10000000000000001、0.1000000000000000055511151231257827021181583404541015625它们在内存中的64bit是完全相同的。计算机在对这些数进行展示的时候,则会选择最短的即0.1进行展示
如果将上面罗列的三个数在控制台中复制给某个变量,然后将变量进行输出,那么我们得到的都会是0.1
const a = 0.1
const b = 0.10000000000000001
const c = 0.1000000000000000055511151231257827021181583404541015625
console.log(a) // 0.1
console.log(b) // 0.1
console.log(c) // 0.1
重回 6.6 + 1.3 的例子
因为7.8999999999999995与7.89999999999999946709在内存中的64bit是完全相同的,计算机在展示的时候会选择最短的7.8999999999999995来展示
const d = 7.89999999999999946709
const e = 7.8999999999999995
console.log(d) // 7.8999999999999995
console.log(e) // 7.8999999999999995
参考资料
《计算机组成原理》