javascript的精度问题

前言

刚接触js的时候就知道了精度的问题,比如下面这个经典的:

1
2

0.1 + 0.2 // 0.30000000000000004

还记得当时查了下资料,知道了js是使用的IEEE 754的双精度浮点类型,并且大家都懂得,2进制嘛,0.1和0.2在二进制中本来就是无限循环的数,所以就有了这些尾巴。但是后来又接二连三的碰到了诸如:

1
2
3
4
0.3 - 0.2 // 0.09999999999999998
500.1 / 10 // 50.010000000000005
10000000000000000 + 1 // 10000000000000000
19.9 * 100 // 1989.9999999999998

本质我知道都是无法在有限的位数表达数字的问题。

再接着

1
2
3
4
5

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324
Math.pow(2, 1024) // Infinity
Number.MAX_SAFE_INTEGER // 9007199254740991

这些值又是怎么确定的呢?hmm…,突然就想搞清楚这到底是怎么一回事了,那么就开始吧!

IEEE-754

一些约定

  • $e$代表指数位数,比如单精度浮点值为8, 双精度为11
  • $exponent$代表指数位数对应的值, 比如当双精度浮点数指数位数为00000000011时,$exponent = 2^1 + 2^0 = 3$
  • 数字下尾带了2的是二进制数,比如$11_2$是十进制的3
  • 如无特别说明, 所有结果都来自mac chrome 66.0.3359.181 64位版本

概述

IEEE-754是二进制浮点数的标准,其中包含了单精确度(32位)、双精确度(64位)、延伸单精确度与延伸双精确度(copy from wiki)。这里我们就只关注双精度浮点数吧,毕竟,我们还是在讨论js的嘛。

一个浮点数可以用以下公式来表示:

$$ float = sign * fraction * exponent $$

也就是说,浮点数的值等于符号位 * 分数值 * 指数值,其实这很好理解,我们就理解为用2进制的科学计数法就行了嘛。在IEEE-754中,双精度浮点数共计64位,由以下三部分组成:

  • 1位符号位

符号位决定了这个数的正负,0为正,1为负。

  • 11位指数位

11位数如果按照有符号来解释,那么范围是-1024 到 1023;如果按照无符号型来解释,范围就是0 到 2047。IEEE-754中,采用后者加上偏移值来表示指数位,这个偏移值为$2^{e-1}-1$。所以双精度中偏移值为$2^{11-1}-1=1023$。所以,实际上浮点数的指数值为$2^{exponent-1023}$。本来,$exponent-1023$的值应该是-1023 到 1024,但是-1023(指数位全部为0)和1024(指数位全部为1)被保留用作表示特殊数字,这一点我们后面会提到。所以,$exponent-1023$的取值范围是-1022 到 1023。

  • 52位分数位(其实共计53位,但是第一位1被省略)

52位分数位指的是计数法的小数部分,但是省略了一个1,所以其实分数位段实际代表的值是二进制的 $1.b_{51}b_{50}…b_0$。

综上,双精度浮点的64位如下图所示(转自wikipedia):

所以我们可以得出如下公式:

$$ float = (-1)^{sign}\left(1 + \sum_{i=1}^{52}b_{52-i}2^{-i}\right)\times2^{exponent-1023} $$

但是上述表达式只是针对规约浮点数的,针对非规约浮点数,我们有另外一个公式:

$$ subnormal\ \ float = (-1)^{sign}\left(\sum_{i=1}^{52}b_{52-i}2^{-i}\right)\times2^{1-1023} $$

规约形式的浮点数

“规约”是指用唯一确定的浮点形式去表示一个值。在IEEE-754标准中,当指数部分的实际值exponent的范围为$0 < exponent < 2^e - 1$时,这个数就叫做规约形式的浮点数。还记得我们在前面说过双精度浮点数的指数范围e是-1022 到 1023吧?所以exponent的范围为1到2046时,代表规约形式的浮点数,当exponent为0和2047时,有别的含义,接着向下看。

特殊值

上面提到exponent为0和2047时有特殊的含义,在规范中,有三个特殊的值:

  • 当exponent为0,且小数部分为0时,浮点数值为±0(取决于符号位)
  • 当exponent为$2^e-1$,且小数部分为0时,浮点数值为无穷±Inifinity
  • 当exponent为$2^e-1$,且小数部分不为0时,代表NaN

所以现在知道我们之前提到为什么规约形式的exponent不能为0和2047了吧。

非规约形式的浮点数(subnormal numbers)

当$exponent = 0$时,且小数部分不为0,这就是非规约形式的浮点数。它用来解决填补绝对值意义下最小规格数与零的距离。非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式溢出(gradual underflow)的力荐。

我们假设没有非规约浮点数,那么针对双精度浮点数,规约形式的最小浮点数为$a = 1.0 * 2^{-1022}$(分数部分全0),离它最近的规约浮点数则为$b = 1.0*(1.0+2^{-52})*2^{-1022}$(分数部分为0000…0001)。那么a与0之间的距离,是a与b之间的距离的$2^{52}$倍!可以说是非常快速的就下溢到了0。这样可能导致一种情况就是,两个非常小的浮点数相减的结果会变成0。

采用非规约形式后,当$exponent = 0$时,且小数部分不为0时,我们称之为非规约形式的浮点数,满足在文章前面提到的表达式:

$$ subnormal\ \ float = (-1)^{sign}\left(\sum_{i=1}^{52}b_{52-i}2^{-i}\right)\times2^{1-1023} $$

讲道理我也不知道为什么这里指数全为0的情况下,计算的时候却是$2^{1-1023}=2^{-1022}$, 当作一种规范吧。

舍入模式

0.1的二进制是$0.000\overline{1100}$的无限循环, 那么显然的,52位的分数位并不能存入这些无限循环的小数位,那么势必就要采取舍入。IEEE-754共有5种舍入模式:

Mode / Example Value +11.5 +12.5 −11.5 −12.5
to nearest, ties to even +12.0 +12.0 −12.0 −12.0
to nearest, ties away from zero +12.0 +13.0 −12.0 −13.0
toward 0 +11.0 +12.0 −11.0 −12.0
toward +∞ +12.0 +13.0 −11.0 −12.0
toward −∞ +11.0 +12.0 −12.0 −13.0

默认采用的舍入模式为: Round to nearest, ties to even。在10进制里很好理解,但是在二进制中,可能就稍微有点怪了。查阅了一些资料,二进制模式下应该是这样的:

浮点数的分数位是$0.b_{51}b_{50}…b_0$,共52位。对于一些需要超过52位来表示的数字,我们假设有如下形式

$$0.b_{51}b_{50}…b_0a_0a_1a_2…a_n$$

注意,这并不代表它一定是无限循环的。现在,由于双精度浮点数只能保留52位分数,所以我们只能保留到$b_0$那一位。

  • 若$a_0$为0(即$a_0$为0,$a_1到a_n$为任意值)

舍去(round down,根据Round to nearest原则)

  • 若$a_0$为1,并且$a_1到a_n$中至少有一个为1

升位(round up,根据Round to nearest)

  • 若$a_0$为1,并且$a_1到a_n$都为0

此时, 无论round up还是round down,得到的结果和原数值的距离都是一样的,所以Round to nearest规则已不适用,我们需要使用ties to even规则了。在二进制中,只要最后一位是0,那么就是even。

所以在这个情况下,若$b_0$为0,那么我们就round down; 如果$b_0$为1,那么我们就round up。

看到这里,我们就以0.1来举例吧。0.1用二进制表示为: $0.000\overline{1100}$, 根据双精度浮点数的标准,则表示为:

$$ (-1)^0 * 1.\underbrace{1001100…11001}_{52位}\ 100\overline{1100} * 2^{-4} $$

但是,我们只有52位。根据上面提到的策略,则0.1在IEEE-754双精度中实际会舍入为如下表达:

$$ (-1)^0 * 1.\underbrace{1001100…11010}_{52位} * 2^{-4} $$

64位整体结构如下图:

$$ \underbrace{0} _ {标志位}\underbrace{01111111011} _ {指数位}\ \underbrace{1001100…11010} _ {52位} $$

规则总结

所有规则总结如下:

浮点数形式或值 指数exponent值 分数部分值
±0 0 0
非规约形式 0 非0
规约形式 1到$2^e-2$ 任意
±Inifinity $2^e-1$ 0
NaN $2^e-1$ 非0

回到正题

好了,在看了上一章节的内容,我们开始试着解释这些“灵异”现象吧!下面我们先有请三位运动员出场:

十进制值 指数值 分数部分52位 计算式
0.1 $2^{1019-1023}=2^{-4}$ $10011001…10011010_2$ $-1^0 * 1.100…11010_2*2^{-4}$
0.2 $2^{1020-1023}=2^{-3}$ $10011001…10011010_2$ $-1^0 * 1.100…11010_2*2^{-3}$
0.3 $2^{1021-1023}=2^{-2}$ $00110011…0011_2$ $-1^0 * 1.100…11010_2*2^{-2}$

Mission 1

先来看看加法

1
2

0.1 + 0.2 // 0.30000000000000004

然后我们来做做二进制加法:

$$ \begin{align}
& \quad 0.00011001100110011001100110011001100110011001100110011010 _ 2 \\ & + 0.00110011001100110011001100110011001100110011001100110100 _ 2 \\ & = 0.01001100110011001100110011001100110011001100110011001110 _ 2 \\ & = 1.\underbrace{001100…11001100111_2} _ {53位} * 2^{-2} \end{align} $$

显然,运算的结果超出了界限,根据之前提到的舍入原则,我们最终得到的结果为:

$$ 1.\underbrace{001100…1100110100_2}_{52位} * 2^{-2} $$

ok, 那么分数部分值就是:

$$1.0 + 2^{-3} + 2^{-4} + 2^{-7} + 2^{-8} + … + 2^{-47} + 2^{-48} + 2^{-50}$$

算出来值为

$$1.20000000000000017763568394002504646778106689453125$$

注意,这个值在二进制里可是可以精确算出来的哦。

再乘以指数$2^{-2}$,那么结果就是

$$0.3000000000000000444089209850062616169452667236328125$$

IEEE-754规定,双精度浮点数默认为我们保留到17位有效数字。所以其实真正的值是下图:

1
2
3
4

0.1 + 0.2 // 0.30000000000000004
(0.1 + 0.2).toFixed(100) // 0.3000000000000000444089209850062616169452667236328125 后面的0省略
0.3.toFixed(100) // 0.299999999999999988897769753748434595763683319091796875 后面的0省略

至于上图0.3为啥是那个值,就不用我多说了吧?

Mission 2

看了加法,我们来看看减法好了

1
2
3

0.3 - 0.2 // 0.09999999999999998
0.2 - 0.1 // 0.1

等等, 0.2 - 0.1竟然是正确的!?。醒醒,怎么可能,这不过是四舍五入恰好变成了0.1罢了。

1
2
3
4

(0.3-0.2).toFixed(100) // 0.09999999999999997779553950749686919152736663818359375 后面的0省略

(0.2-0.1).toFixed(100) // 0.1000000000000000055511151231257827021181583404541015625 后面的0省略

我们就再详细看看0.2-0.1吧:

$$\begin{align}
& \quad 0.00110011001100110011001100110011001100110011001100110100_2 \\ & - 0.00011001100110011001100110011001100110011001100110011010_2 \\ & = 0.00011001100110011001100110011001100110011001100110011010_2 \\ & = 1.\underbrace{1001100…110011010_2}_{53位} * 2^{-4} \end{align} $$

其实我们可以看出来,再经过了舍入后,0.2和0.1依然保持了两倍的关系,所以相减的值其实就是0.1在双精度浮点数中代表的值,所以,下面的等式是成立的:

1
2

0.1.toFixed(100) == (0.2-0.1).toFixed(100) // 0.1000000000000000055511151231257827021181583404541015625 后面的0省略

Mission 3

再来看看乘法0.1 * 0.2

1
2

0.1 * 0.2 // 0.020000000000000004

$$\begin{align}
& \quad 0.00110011001100110011001100110011001100110011001100110100_2 \\ & * 0.00011001100110011001100110011001100110011001100110011010_2 \\ \end{align} $$

这乘法手算头有点痛啊,写个代码算一下结果是:

1
2

0.0000010100011110101110000101000111101011100001010001111011 100001010001111010111000010100011110101110000101001000

得到如下结构:

$$ 1.\underbrace{0100011110101110000101000111101011100001010001111011_2}_{52位}100001… * 2^{-6} $$

再根据舍入规则,我们round up,得到:

$$ 1.\underbrace{0100011110101110000101000111101011100001010001111100_2}_{52位}* 2^{-6} $$

然后我们来算一算:

$$2^{-6} + 2^{-8} + 2^{-12} + 2^{-13} + 2^{-14} + … + 2^{-54} + 2^{-55} + 2^{-56}$$

结果为: 0.02000000000000000388578058618804789148271083831787109375

1
2
3

0.1 * 0.2 // 0.020000000000000004
(0.1 * 0.2).toFixed(100) //0.02000000000000000388578058618804789148271083831787109375

和js的一模一样。

Mission 4

我们最后再看看一些边界值的情况吧。

  • 无穷大

    规范已经规定了是Infinity,就是在指数位最大,分数位为0的情况下满足,所以这也就解释了为什么如下的结果:

    1
    2

    Math.pow(2,1024) // Infinity
  • 最大值

    当指数位为1023,分数位全部为1时,我们可以取得最大的浮点数值:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{11111111110} _ {指数位}\ \underbrace{111…111} _ {52位} \\ & = 2^{1023} * (1 + (1 - 2^{-52} )) \\ & \approx 1.7976931348623157e+308 \end{align}$$

    也就是Number.MAX_VALUE

  • 最小正数

    当指数位全部为0,分数位为1,我们得到js可以表示的最小正数,更准确的说,应该是最小非规约浮点数:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{00000000000} _ {指数位}\ \underbrace{000…001} _ {52位} \\ & = 2^{-1022} * 2^{-52} \\ & = 2^{-1074} \\ & \approx 4.9*10^{−324} \end{align}$$

    然后在js中,在展示的时候应该是做了四舍五入:

    1
    2
    3
    4

    Number.MIN_VALUE // 5e-324
    Math.pow(2,-1074) // 5e-324
    Math.pow(2,-1075) // 0
  • 最大的安全整数

    按照前面的叙述,我们总共有53位的分数位(包含隐去的第一位1),所以我们能表达的最精确的整数就是当分数位全部为1,然后指数位为52的时候,也就是:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{10000110011} _{ 指数位}\ \underbrace{111…111} _ {52位} \\ & = 2^{53} -1 \\ & = 9007199254740991 \end{align}$$

    当从这个整数开始逐步加1,就有可能丢失有效值,所以就有了最大安全整数的概念。ecma的es6规范定义Number.MAX_SAFE_INTEGER如下:

    The value of Number.MAX_SAFE_INTEGER is the largest integer n such that n and n + 1 are both exactly representable as a Number value.The value of Number.MAX_SAFE_INTEGER is 9007199254740991 ($2^{53}−1$).

    有趣的是,chrome里这个值是9007199254740992。但是这个数是不符合es6规范的,因为9007199254740992 + 1 仍然是 9007199254740992。其实我猜测chrome可能想表达的意思是9007199254740992依然可以准确的表达, 这里大家懂了就行。

    下面我们来看看为什么有如下的情况吧:

    1
    2
    3

    9007199254740992 + 1 // 9007199254740992
    9007199254740992 + 3 // 9007199254740996

    9007199254740993表示如下:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{10000110100} _ {指数位}\ \underbrace{000…000} _ {52位}\ 1 \\ \end{align}$$

    虽然分数位52位是0,53位是1,根据ties to even原则,这个1也要被省略,所以9007199254740993 == 9007199254740992为true

    9007199254740995表示如下:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{10000110100} _ {指数位}\ \underbrace{000…001} _ {52位}\ 1 \\ \end{align}$$

    根据ties to even原则round up,得到:

    $$\begin{align} & \quad \underbrace{0} _ {标志位} \underbrace{10000110100} _ {指数位}\ \underbrace{000…001} _ {52位}\ 1 \\ & = \underbrace{0} _ {标志位} \underbrace{10000110100} _ {指数位}\ \underbrace{000…010} _ {52位}\ \\ & = 2^{53} * 1.\underbrace{000…010} _ {52位} \\ & = 9007199254740992 + 4 \\ & = 9007199254740996 \end{align}$$

总结

一堆0和1,看的我有点头晕了😄。最后,相信你在看了上面的内容后,能够解释下面这个例子里toFixed函数失效的原因了吧?

1
2

1.335.toFixed(2) // 1.33

参考资料

  1. wikipedia IEEE-754 IEEE-754标准
  2. wikipedia double precision floating point format 双精度浮点数
  3. big.js 一个用于数学计算的js库
  4. IEEE-754 计算器 一个用js实现的在线计算IEEE-754标准的网页