JS基础 | 浮点数计算原理剖析

1,715 阅读5分钟

前言

此文参照计算机组成原理从底层剖析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.610x= (-1)0 *(1.1010011001100110011001100110011001100110011001100110)*22
1.310x=(-1)0*(1.0100110011001100110011001100110011001100110011001101)*20
64位存储

原数据在计算机中的存储则如下表所示

原数据符号位S阶码E尾数M备注
6.6100100000000011010011001100110011001100110011001100110011001100110最后一位舍入00 => 0
1.3100011111111110100110011001100110011001100110011001100110011001101最后一位舍入01 => 1
重新翻译成十进制

对上表中的二进制重新翻译成十进制如下表所示

原数据舍入后十进制
6.6106.5999999999999996447310
1.3101.3000000000000000444110

对阶

由上述表格可知,数值6.6在计算机中的阶码大于数值1.3在计算机中的阶码,因此需调整数值1.3的指数,使其阶码与数值6.6的阶码对齐

原数符号位S阶码E尾数M
6.6100100000000011010011001100110011001100110011001100110011001100110
1.3100100000000010101001100110011001100110011001100110011001100110011

尾数相加

	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.11000111111 10111001100110011001100110011001100110011001100110011010

当我们使用toPrecision(17)获取0.1的精度,可以得到0.10000000000000001

那么针对0.1为什么控制台不显示0.10000000000000001而显示0.1呢?

因为计算机中许多不同的十进制数共享相同的最接近的近似二进制小数,而在展示上,大多数系统上会选择这些不同的十进制数中最短的来展示。

回到0.1的例子中,其实存在着十进制数0.10.100000000000000010.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.89999999999999957.89999999999999946709在内存中的64bit是完全相同的,计算机在展示的时候会选择最短的7.8999999999999995来展示

const d = 7.89999999999999946709
const e = 7.8999999999999995
console.log(d) // 7.8999999999999995
console.log(e) // 7.8999999999999995

参考资料

《计算机组成原理》

十进制在线转换为IEEE754 64位二进制

二进制在线转十进制

浮点计算精度损失原因

JavaScript 浮点数陷阱及解法

浮点数的表示中为什么要用移码表示阶码

从计组课到前端深坑:IEEE 754双精度浮点数的那些事