Skip to content

Javascript浮点数

有一道很常见的面试题

0.1 + 0.2 === 0.3 // true ? false

大家应该都知道是 false,但是为毛不相等呢?下面我将从 浮点数表示浮点数精度 两个方面来解释

Javascript 浮点数表示

Javascript 中不存在整型和浮点型之分,只有一个类型 Number,它遵循 IEEE 二进制浮点数算术标准(IEEE754),使用 64 位 双精度浮点数(double) 存储。

双精度存储是知道了,那双精度浮点数是如何存储数据呢?主要有以下几个关键点

1. 双精度浮点数使用 64 位存储

  • sign(S):符号位,长度为 1,0 代表数字为正,1 代表数字为负
  • exponent(E):指数位,长度为 11,二进制科学计数法的指数位
  • mantissa(M):尾数位,长度为 52 ,二进制科学计算法的尾数位

数值的计算公式为(二进制):

value=(1)S2EMvalue = (-1)^S * 2^E * M

2. 使用二进制科学计数法

举个栗子: 0.5100.5_{10} 转换为二进制表示 0.120.1_2 0.120.1_2 转换为二进制科学计数法 1211*2^{-1}

3. 指数位表示有符号整数

因为指数位数值为无符号整数,范围为[0, 2047],在规格化值中取 [1, 2046]。但指数位需要表示的是有符号整数,则[1, 1022] 表示为负数[-1022, -1],1023 表示为 0,[1024, 2046]表示为正数[1, 1023],所以整个指数位表示的范围是[-1022, 1023]

由上可推导出公式:

E=eBias=e1023E = e - Bias = e - 1023

  • E:指数位表示的数值
  • e:指数位实际存储的数值

举个栗子: 0.5100.5_{10}表示为二进制科学计数法1211*2^{-1},该数 E = -1,e = 1022

tips: 下面会讲什么是规格化值,和其他的值

4. 尾数不表示二进制科学计算的整数部分,即不表示 1

由于规格化的数整数位都是 1,所以在存储时可以节约空间,不表示整数位,从小数位开始表示。所以尾数位其实最大能表示 53 位

由上可推导出公式:

M=1+fM = 1 + f

  • M:位数为表示的数值
  • f:位数为实际存储的数值

举个栗子: 二进制科学计数法1211*2^{-1},该数 M=1,则实际存储 f=0,

由上可将数值计算公式推导为

value=(1)S2e1023(f+1)value = (-1)^S * 2^{e-1023} * (f + 1)

非规格化值 和 特殊值

那么问题来了,如果按公式 M=f+1M = f + 1,那么如何表示 0 呢?,即使你设 f = 0,e=任意值,按照公式算出,value 不可能为 0。当然第一个公式 value=(1)S2EMvalue = (-1)^S * 2^E * M 没有问题,问题出在公式的推导,因为推导出的公式只符合规格化数值,下面介绍下规格化数值和其他数值

  • 规格化数值:e 不全为 0,也不全为 1,f 为任意值
  • 非规格化数值:e 全部为 0,f 为任意值。非规格化数值主要用于表示 0,以及接近 0 的数。此时公式为 E=e=0M=fE = e = 0,M = f value=(1)Sfvalue = (-1)^S * f
  • 无穷大:e 全部为 1,f 为 0
  • NaN:e 全部为 1,f 不为 0

所以:当 e = 0 时,f = 0 时,表示数值 0

解释 0.1 + 0.2 !== 0.3

到这里,我们已经可以解释 0.1 + 0.2 为什么不等于 0.3 咯

0.1100.1_{10} 表示为二进制为:0.0001100...1100...1100..._20.0001100...1100...1100...\_2 转换为二进制科学计数法为:1.1001100...1100...1100...241.1001100...1100...1100...*2^{-4} 计算得:S = 0,E = -4,M = 1.1001100…1100…1100…,则 S = 0,e = 1029,f = 1001100…1100…11010 将截断(舍入)后的数值重新表示为二进制,则 0.1 最终的二进制数值为: 0.00011...0011...001101

同理,表示出 0.2,0.3 的二进制数值

0.1:0.0001100110011001100110011001100110011001100110011001101
0.2:0.001100110011001100110011001100110011001100110011001101
0.3:0.010011001100110011001100110011001100110011001100110011
0.1 + 0.2 和为: // 下面会详情介绍该步骤
0.0100110011001100110011001100110011001100110011001101
将和与0.3对比,发现并不相等,中间差值为:
0.000000000000000000000000000000000000000000000000000001

从结果值来看,中间差值已经很小很小了,已经可以忽略了不计了。事实上在 ES6 Number 扩展中,增加 Number.EPSILON 属性,表示 1 与大于 1 的最小浮点值差,值为 2522^{-52},当值小于 Number.EPSILON 时,一般可忽略不计

浮点数精度

舍入

尾数位只能存储 52 位,但是在 0~1 之间的实数是无穷尽的,这些无穷的数该如何表示呢?既然完全表示不可能完成,那么只有舍弃掉某些数值,来找出最近的浮点数匹配。那么到底采用哪种舍入方法呢?下面介绍常用的舍入方法

  • 向偶数舍入:也称为向最接近值舍入
  • 向零舍入:舍弃末尾位以外的数值,可以按照 Math.trunc 理解
  • 向正无穷舍入:向较大的数舍入,可以按照 Math.ceil 理解
  • 向负无穷舍入:向较小的数舍入,可以按照 Math.floor 理解

IEEE754 采用的是 向偶数舍入,原则是保证损失精度最小,下面简单介绍一下舍入规则

  1. 对于恰巧中间值的情况,如果保留位数最后一位是偶数则舍弃后续数值,奇数则进位。例如 1.0121.01_2 保留 1/2 为 1.021.0_21.1121.11_2 保留 1/2 为 1.221.2_2
  2. 对于向上的值较近,则进位。如 1.010101011.01010101 保留 1/2 为1.121.1_2
  3. 对于向下的值较近,则舍弃。如 1.0011.001 保留 1/2 为 1.021.0_2

举个栗子:

0.1 => 0.0001100110011001100110011001100110011001100110011001100 | 110011...
// 由于需要保留的最后一位数后为 110011...,舍入时离向上的值较近,应该进位,所以
0.1 => 0.0001100110011001100110011001100110011001100110011001101
  • ”|” 表示在该处需要舍入

tips:如果你实在无法判断如何舍入,有个简单的办法。先向下舍入,与原数相减;再向上舍入,与原数相减,将两个差值比较,取差值绝对值较小的那个数;如果差值相等,则取末尾为偶数的那个数

计算

为了进一步保证精度,IEEE754 标准,双精度在中间计算时,额外保留三位,分别是 保护位、舍入位、粘贴位

  • 保护位:精度最低位右侧一位(双精度可以理解为尾数的第 53 位)
  • 舍入位:保护位右侧一位
  • 粘贴位:舍入位右侧一位,代表舍入位右侧是否还有数据,如果右侧还有数据,则粘贴位为 1,否则为 0,目的是为了支持目标数值向最近的偶数舍入

在浮点数计算时,通过额外保存三位,来增加计算的正确性,找到浮点数最接近的匹配。

模拟 0.1 + 0.2 计算

讲了那么多,感觉没什么用,最后还是简单写一下 0.1 + 0.2 的二进制计算过程。

0.1:S = 0,E = -4,M = 1.1001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010
// 对阶 小阶对大阶
0.1:S = 0,E = -3,M = 0.11001100110011001100110011001100110011001100110011010
0.2:S = 0,E = -3,M = 1.1001100110011001100110011001100110011001100110011010
// 将 M 值相加
和为:10.01100110011001100110011001100110011001100110011001110
计算出的0.3:S = 0,E = -3,M = 10.01100110011001100110011001100110011001100110011001110
// 规格化
计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011 | 10
// 计算时,右边多保留两位,此处保护位 = 1,舍入位 = 0,粘贴位 = 0
// 舍入后
计算出的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110100
浮点数的0.3:S = 0,E = -2,M = 1.0011001100110011001100110011001100110011001100110011
// 转换10进制
计算出的0.3 = 0.30000000000000004
  • ”|” 表示应该截断,后续数值即为保护位,舍入位,粘贴位

参考

  • 《深入理解计算机系统》第二章节
  • 《计算机组成与设计 软件/硬件接口》 第三章节
  • 维基百科 IEEE 754