3. 真实的值(读时)
编程语言是如何读取并显示内存中的 binary64 数据的
Last updated
编程语言是如何读取并显示内存中的 binary64 数据的
Last updated
理解了双精度浮点数的格式和含义以及它在存储时是会损失精度的,在此基础上,我们继续探索下编程语言是如何读取并显示内存中的 binary64 数据的,以 JavaScript 为例。
继续以十进制的 0.1 为例,我们知道了它在内存中存储的真实数值并不是 0.1 而是 0.100000000000000005551115123126,其 IEEE 754 binary64 的内存存储如下:
但是,在我们的日常开发中,0.1 是总会输出 0.1 的,而不是它在内存中的精确值,除非我们手动指定精度。代码如下:
也就是说,在我们的日常使用中,编程语言会自动帮我们截取精度,以让显示的数值“看起来”是正确的。
那么,双精度浮点数是按照什么规则来截取精度值的呢?
维基百科中这样写道:
The 53-bit significand precision gives from 15 to 17 significant decimal digits precision ( ≈ 1.11 × ). If a decimal string with at most 15 significant digits is converted to IEEE 754 double-precision representation, and then converted back to a decimal string with the same number of digits, the final result should match the original string. If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number.
直译就是:IEEE 754 binary64 里的 53 位有效数精度能提供十进制数的 15 到 17 个有效数精度。
如果将一个最多有 15 位有效数字的十进制字符串转成 IEEE 754 双精度表示,然后再转回有相同位数的十进制字符串,则最终的结果应该(should)会和原始字符串相匹配。
如果将一个 IEEE 754 双精度数值转成一个至少有 17 位有效数字的十进制字符串,然后再将它转回双精度表示,则最终的结果一定(must)会和原始数值相匹配。
大意就是:
存十进制数时,如果它的有效数的位数 ≤15,那么计算机就能(should)保证其“存&取”一致。
读内存里的 IEEE 754 binary64 时,如果十进制有效数的位数 ≥17,那么计算机就能(must)保证其“取&存”一致。
这部分内容旨在讨论编程语言是如何读取并显示内存中的 binary64 数据的,所以我们只重点关注第 2 条,即:读内存中的数值时,只要十进制的有效数字是 17 个就能保证不出错(内存里存啥就显示啥)。
来看几个例子感受下。
那么,为什么“16”就能刚好让数值看起来是正确的,而“17”就是一个照妖镜呢?
在回答这个问题之前,我们先来看下二进制的 n 个有效位表示成十进制是什么样子的。
为了更清晰地描述问题,我们将整数部分和小数部分分开讨论,然后分别都从 1 个有效位开始,再逐渐增加有效位的个数,以便观察在这个过程中发生的变化。
当只有 1 个二进制有效位时 x
:能表示 2 个十进制数,它们之间的差值是 1。如下:
0
0
1
1
当有 2 个二进制有效位时 xx
:能表示 = 4 个十进制数,它们之间的差值是 1。如下:
00
0
01
1
10*
2*
11*
3*
其中,* 表示多了一个有效位之后新增的数,新增了 个,其高位均是 。
当有 3 个二进制有效位时 xxx
:能表示 = 8 个十进制数,它们之间的差值是 1。如下:
000
0
001
1
010
2
011
3
100*
4*
101*
5*
110*
6*
111*
7*
其中,* 表示多了一个有效位之后新增的数,新增了 = 个,其高位均是 。
综上,利用数学归纳法可得出以下结论:当整数部分有 n 个二进制有效位时
可以表示 个十进制数,且它们是个等差数列,差值是 1
每增加 1 个二进制有效位,就会新增 个十进制数(即高位为 1 的数值)
当只有 1 个二进制有效位时 0.x
:能表示 2 个十进制数,它们之间的差值是 = 0.5。如下:
0.0
0
0.1
0.5
当有 2 个二进制有效位时 0.xx
:能表示 = 4 个十进制数,它们之间的差值是 = 0.25。如下:
0.00
0
0.01*
0.25*
0.10
0.5
0.11*
0.75*
其中,* 表示多了一个有效位之后新增的数,新增了 个,其末位均是 。
当有 3 个二进制有效位时 0.xxx
:能表示 = 8 个十进制数,它们之间的差值是 = 0.125。如下:
0.000
0
0.001*
0.125*
0.010
0.25
0.011*
0.375*
0.100
0.5
0.101*
0.625*
0.110
0.75
0.111*
0.875*
其中,* 表示多了一个有效位之后新增的数,新增了 = 个,其末位均是 。
当有 4 个二进制有效位时 0.xxxx
:能表示 = 16 个十进制数,它们之间的差值是 = 0.0625。如下:
0.0000
0
0.0001*
0.0625*
0.0010
0.125
0.0011*
0.1875*
0.0100
0.25
0.0101*
0.3125*
0.0110
0.375
0.0111*
0.4375*
0.1000
0.5
0.1001*
0.5625*
0.1010
0.625
0.1011*
0.6875*
0.1100
0.75
0.1101*
0.8125*
0.1110
0.875
0.1111*
0.9375*
其中,* 表示多了一个有效位之后新增的数,新增了 = 个,其末位均是 。
综上,利用数学归纳法可得出结论:当小数部分有 n 个二进制有效位时
可以表示 个十进制数,且它们是个等差数列,差值是 ,数列范围是 [, 1)
与纯整数不同,二进制只能精准地表示一部分十进制小数 ,其中 0 ≤ k < - 1,k 是整数
而其它小数只能是一个无限逼近真实值的二进制值,随着 n 越来越大时
每增加 1 个二进制有效位,就会新增 个十进制数(即末位为 1 的数值)
当有 n 个二进制有效位(可能同时包含整数部分和小数部分)时:
可以表示 个十进制数,且会形成一个等差数列
整数部分的差值是 1
小数部分的差值是 ,小数将是“不连续”的(相对整数而言)
每增加 1 个二进制有效位,就会新增 个十进制数
以上结论,借鉴到 IEEE 754 双精度浮点数上就是:
IEEE 754 双精度浮点数能表示的小数们只有少部分是精准无误的,其它的都只能是一个无限逼近真实值的二进制数值。
IEEE 754 双精度浮点数的精度只和 53 个有效位有关,和指数无关。虽然小数点的位置可以随着指数的值向左/向右“无限”移动,但因为有效位只有 53 个,所以只能用 0 来补齐,反过来说就是我们没法弥补被舍弃位置本可以是 1 的情况。
IEEE 754 binary64 的存储格式就意味着:
符号位决定了数值的正负
指数决定了数值的量级,即最终值的绝对值是超级无敌小,还是超级无敌大
有效位决定了数值有效数字的个数
假设二进制指数的实际值是 e,最终的十进制数值为 x,那么:
当 e<0 时,|x| ∈ (0,1),纯小数
当 e=0 时,|x| ∈ [1,2),有整数有小数
当 e>0 时,|x| ≥ 2,有整数有小数
当 e≥52 时,|x| 就是纯整数了
接下来,让我们看看当有 n 个二进制位时,它能表示多少个十进制数,以及十进制数的有效位的情况。
二进制的不同位数,能表示的最大十进制数。如下:
1
- 1 = 1
< 1e1
11
- 1 = 3
< 1e1
111
- 1 = 7
< 1e1
1111
- 1 = 15
< 1e1 < 1e2
11111
- 1 = 31
< 1e2
111111
- 1 = 63
< 1e2
1111111
- 1 = 127
< 1e2 < 1e3
11111111
- 1 = 255
< 1e3
111111111
- 1 = 511
< 1e3
1111111111
- 1 = 1023
< 1e3 < 1e4
11111111111
- 1 = 2047
< 1e4
111111111111
- 1 = 4095
< 1e4
1111111111111
- 1 = 8191
< 1e4
11111111111111
- 1 = 16383
< 1e4 < 1e5
111111111111111
- 1 = 32767
< 1e5
1111111111111111
- 1 = 65535
< 1e5
11111111111111111
- 1 = 131071
< 1e5 < 1e6
从上表可以看出,二进制的 n 个有效位可以表示十进制的 m 个有效位,用公式表示就是:
< - 1 < ,即 m-1 < lg( - 1) < m
所以,在 IEEE 754 binary64 里,当 53 个有效位均为整数部分时,lg(-1) < 16,即它能表示的十进制将最多有 16 个有效数字。
二进制的不同位数,能表示的十进制数列的差值。如下:
0.1
= 0.5 = 5e-1
> 0.1 = 1e-1
0.01
= 0.25 = 2.5e-1
> 0.1 = 1e-1
0.001
= 0.125 = 1.25e-1
> 0.1 = 1e-1
0.0001
= 0.0625 = 6.25e-2
> 0.01 = 1e-2
0.00001
= 0.03125 = 3.125e-2
> 0.01 = 1e-2
0.000001
= 0.015625 = 1.5625e-2
> 0.01 = 1e-2
0.0000001
= 0.0078125 = 7.8125e-3
> 0.001 = 1e-3
0.00000001
= 0.00390625 = 3.90625e-3
> 0.001 = 1e-3
0.000000001
= 0.001953125 = 1.953125e-3
> 0.001 = 1e-3
0.0000000001
= 0.0009765625 = 9.765625e-4
> 0.0001 = 1e-4
从上表可以看出,二进制的 n 个有效位可以表示的十进制数列的差值的取值范围 < ,也就是说至少需要 m 个十进制有效位。最多需要几个呢?答案是 n 个,因为差值的小数点后有几位,能表示的十进制数列的有效数字最多就有几位。
所以,在 IEEE 754 binary64 里,当 53 个有效位均为小数部分时, ≈ -1.11e-16 > ,即它能表示的十进制小数最少要有 16 个有效数字。
而不连续的小数,需要再多 1 位有效数字来进行四舍五入,故小数部分需要 17 个有效数字。
这部分内容虽然篇幅较长,但其实就解释了一句话,就是:读内存中的 IEEE 754 binary64 数值时,只要十进制的有效数字是 17 个就能保证“内存里存啥就显示啥”。用代码表述就是 toPrecision(17)
能显示出内存中的真实值,而 toPrecision(16)
能让小数“看起来”是对的。
IEEE 754 binary64 里的 53 位有效数精度能提供十进制数的 15 到 17 个有效数精度。
整数部分,有效位最多 16 位。 ≈ <
小数部分,有效位最少 16 位,保险 17 位。 < ≈ 1.11e-16