🏦
计算机基础
Web 技术计算机基础数据结构与算法代码设计数据分析Others
  • 字符和字符串
    • 1. Unicode 字符编码模型
  • 浮点数
    • 1. 双精度浮点数
    • 2. 精度损失(存时)
    • 3. 真实的值(读时)
    • 4. 五种异常
    • 5. 四类常量
    • 6. 字面量表示
  • HTTP
    • 📁HTTP 理论
      • 💡HTTP 发展简史
      • 🗒️HTTP 是什么
      • 🗒️网络分层模型
      • 🗒️HTTP 的特点
      • 🗒️HTTP 的内容
    • 📁URL 相关
      • 🗒️DNS
      • ❕URN+URL=>URI
      • ❕URL
      • ❕输入 URL 按下回车后
    • 📁HTTP 报文
      • 🗒️HTTP 报文格式
      • 🗒️request line
      • 🗒️status line
      • 🗒️header
    • 📁HTTP Header
      • 💡HTTP Header 一览表
      • ❕内容协商
      • ❕大文件传输
      • 🗒️连接管理
      • 🗒️重定向
      • 🗒️HTTP Cookie
      • 🗒️代理服务
      • ❕Cache-Control
      • 🗒️条件请求
      • ❕HTTP CORS
      • ❕HTTP Authentication
    • 🗒️HTTP 安全漏洞
    • 🛠️相关工具
      • Wireshark
  • 更多协议
    • 💡WebSocket 简介
    • 🗒️OAuth 2.0
  • Linux
    • 1. 文件系统
      • 1.1 文件的类型和权限
      • 1.2 目录结构
    • 2. 账号管理
      • 2.1 用户名和密码
    • 3. shell
      • 3.1 shell 基础
      • 3.2 初识 shell 和 bash
  • Git
    • 🗒️Git 简介
    • 💡Git 优势特性
Powered by GitBook
On this page
  • 1. 截取精度的规则
  • 2. 从二进制到十进制
  • 2.1 纯整数
  • 2.2 纯小数
  • 2.3 小结
  • 2.4 双精度浮点数
  • 3. 二进制与十进制的对应关系
  • 3.1 整数部分
  • 3.2 小数部分
  • 4. 总结
  1. 浮点数

3. 真实的值(读时)

编程语言是如何读取并显示内存中的 binary64 数据的

Previous2. 精度损失(存时)Next4. 五种异常

Last updated 2 years ago

理解了以及它的,在此基础上,我们继续探索下编程语言是如何读取并显示内存中的 binary64 数据的,以 JavaScript 为例。

继续以十进制的 0.1 为例,我们知道了它在内存中存储的真实数值并不是 0.1 而是 0.100000000000000005551115123126,其 IEEE 754 binary64 的内存存储如下:

但是,在我们的日常开发中,0.1 是总会输出 0.1 的,而不是它在内存中的精确值,除非我们手动指定精度。代码如下:

0.1; // 0.1
Number(0.1); // 0.1

// 当手动指定精度时
Number(0.1).toPrecision(); // '0.1', 参数为空则调 toString()
Number(0.1).toPrecision(16); // '0.1000000000000000'  = 0.1
Number(0.1).toPrecision(17); // '0.10000000000000001' > 0.1
Number(0.1).toPrecision(18); // '0.100000000000000006'
Number(0.1).toPrecision(19); // '0.1000000000000000056'
Number(0.1).toPrecision(20); // '0.10000000000000000555'
Number(0.1).toPrecision(21); // '0.100000000000000005551'
Number(0.1).toPrecision(55); // '0.1000000000000000055511151231257827021181583404541015625'
Number(0.1).toPrecision(70); // '0.1000000000000000055511151231257827021181583404541015625000000000000000'

/**
 toPrecision() 方法返回的类型是字符串
 (1)若精度参数为空或未定义,则会调用 Number.toString() 方法
 (2)若精度不在 [1,100] 之间,则会抛出错误 RangeError
 
 ECMA-262 只需要最多 21 位有效数字的精度
 https://262.ecma-international.org/5.1/#sec-15.7.4.7
 **/

也就是说,在我们的日常使用中,编程语言会自动帮我们截取精度,以让显示的数值“看起来”是正确的。

那么,双精度浮点数是按照什么规则来截取精度值的呢?

1. 截取精度的规则

The 53-bit significand precision gives from 15 to 17 significant decimal digits precision (2−532^{−53}2−53 ≈ 1.11 × 10−1610^{−16}10−16). 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 个有效数精度。

  1. 如果将一个最多有 15 位有效数字的十进制字符串转成 IEEE 754 双精度表示,然后再转回有相同位数的十进制字符串,则最终的结果应该(should)会和原始字符串相匹配。

  2. 如果将一个 IEEE 754 双精度数值转成一个至少有 17 位有效数字的十进制字符串,然后再将它转回双精度表示,则最终的结果一定(must)会和原始数值相匹配。

大意就是:

  1. 存十进制数时,如果它的有效数的位数 ≤15,那么计算机就能(should)保证其“存&取”一致。

  2. 读内存里的 IEEE 754 binary64 时,如果十进制有效数的位数 ≥17,那么计算机就能(must)保证其“取&存”一致。

这部分内容旨在讨论编程语言是如何读取并显示内存中的 binary64 数据的,所以我们只重点关注第 2 条,即:读内存中的数值时,只要十进制的有效数字是 17 个就能保证不出错(内存里存啥就显示啥)。

来看几个例子感受下。

Number(0.2); // 0.2
Number(0.2).toPrecision(16); // '0.2000000000000000' = 0.2
Number(0.2).toPrecision(17); // '0.20000000000000001' > 0.2

Number(1.005); // 1.005
Number(1.005).toPrecision(16); // '1.005000000000000' = 1.005
Number(1.005).toPrecision(17); // '1.0049999999999999' < 1.005

那么,为什么“16”就能刚好让数值看起来是正确的,而“17”就是一个照妖镜呢?

在回答这个问题之前,我们先来看下二进制的 n 个有效位表示成十进制是什么样子的。

2. 从二进制到十进制

为了更清晰地描述问题,我们将整数部分和小数部分分开讨论,然后分别都从 1 个有效位开始,再逐渐增加有效位的个数,以便观察在这个过程中发生的变化。

2.1 纯整数

2.1.1 有 1 个二进制有效位

当只有 1 个二进制有效位时 x:能表示 2 个十进制数,它们之间的差值是 1。如下:

二进制
十进制

0

0

1

1

2.1.2 有 2 个二进制有效位

当有 2 个二进制有效位时 xx:能表示 222^222 = 4 个十进制数,它们之间的差值是 1。如下:

二进制
十进制

00

0

01

1

10*

2*

11*

3*

其中,* 表示多了一个有效位之后新增的数,新增了 2(10)2_{(10)}2(10)​ 个,其高位均是 1(2)1_{(2)}1(2)​。

2.1.3 有 3 个二进制有效位

当有 3 个二进制有效位时 xxx:能表示 232^323 = 8 个十进制数,它们之间的差值是 1。如下:

二进制
十进制

000

0

001

1

010

2

011

3

100*

4*

101*

5*

110*

6*

111*

7*

其中,* 表示多了一个有效位之后新增的数,新增了 4(10)4_{(10)}4(10)​ = 222^222 个,其高位均是 1(2)1_{(2)}1(2)​。

2.1.4 小结

综上,利用数学归纳法可得出以下结论:当整数部分有 n 个二进制有效位时

  1. 可以表示 2n2^n2n 个十进制数,且它们是个等差数列,差值是 1

  2. 每增加 1 个二进制有效位,就会新增 2n−12^n-12n−1 个十进制数(即高位为 1 的数值)

2.2 纯小数

2.2.1 有 1 个二进制有效位

当只有 1 个二进制有效位时 0.x:能表示 2 个十进制数,它们之间的差值是 2−12^{-1}2−1 = 0.5。如下:

二进制
十进制

0.0

0

0.1

0.5

2.2.2 有 2 个二进制有效位

当有 2 个二进制有效位时 0.xx:能表示 222^222 = 4 个十进制数,它们之间的差值是 2−22^{-2}2−2 = 0.25。如下:

二进制
十进制

0.00

0

0.01*

0.25*

0.10

0.5

0.11*

0.75*

其中,* 表示多了一个有效位之后新增的数,新增了 2(10)2_{(10)}2(10)​ 个,其末位均是 1(2)1_{(2)}1(2)​。

2.2.3 有 3 个二进制有效位

当有 3 个二进制有效位时 0.xxx:能表示 232^323 = 8 个十进制数,它们之间的差值是 2−32^{-3}2−3 = 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(10)4_{(10)}4(10)​ = 222^222 个,其末位均是 1(2)1_{(2)}1(2)​。

2.2.4 有 4 个二进制有效位

当有 4 个二进制有效位时 0.xxxx:能表示 242^424 = 16 个十进制数,它们之间的差值是 2−42^{-4}2−4 = 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*

其中,* 表示多了一个有效位之后新增的数,新增了 8(10)8_{(10)}8(10)​ = 232^323 个,其末位均是 1(2)1_{(2)}1(2)​。

2.2.5 小结

综上,利用数学归纳法可得出结论:当小数部分有 n 个二进制有效位时

  1. 可以表示 2n2^n2n 个十进制数,且它们是个等差数列,差值是 2−n2^{-n}2−n,数列范围是 [2−n2^{-n}2−n, 1)

    • 与纯整数不同,二进制只能精准地表示一部分十进制小数 k2n\frac{k}{2^n}2nk​,其中 0 ≤ k < 2n2^n2n - 1,k 是整数

    • 而其它小数只能是一个无限逼近真实值的二进制值,随着 n 越来越大时

  2. 每增加 1 个二进制有效位,就会新增 2n−12^{n-1}2n−1 个十进制数(即末位为 1 的数值)

2.3 小结

当有 n 个二进制有效位(可能同时包含整数部分和小数部分)时:

  1. 可以表示 2n2^n2n 个十进制数,且会形成一个等差数列

    • 整数部分的差值是 1

    • 小数部分的差值是 2−m2^{-m}2−m,小数将是“不连续”的(相对整数而言)

  2. 每增加 1 个二进制有效位,就会新增 2n−12^{n-1}2n−1 个十进制数

2.4 双精度浮点数

以上结论,借鉴到 IEEE 754 双精度浮点数上就是:

  1. IEEE 754 双精度浮点数能表示的小数们只有少部分是精准无误的,其它的都只能是一个无限逼近真实值的二进制数值。

  2. 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 个二进制位时,它能表示多少个十进制数,以及十进制数的有效位的情况。

3. 二进制与十进制的对应关系

3.1 整数部分

二进制的不同位数,能表示的最大十进制数。如下:

二进制
十进制最大数
十进制数的范围

1

< 1e1

11

< 1e1

111

< 1e1

1111

< 1e1 < 1e2

11111

< 1e2

111111

< 1e2

1111111

< 1e2 < 1e3

11111111

< 1e3

111111111

< 1e3

1111111111

< 1e3 < 1e4

11111111111

< 1e4

111111111111

< 1e4

1111111111111

< 1e4

11111111111111

< 1e4 < 1e5

111111111111111

< 1e5

1111111111111111

< 1e5

11111111111111111

< 1e5 < 1e6

从上表可以看出,二进制的 n 个有效位可以表示十进制的 m 个有效位,用公式表示就是:

10m−110^{m-1}10m−1 < 2n2^n2n - 1 < 10m10^m10m,即 m-1 < lg(2n2^n2n - 1) < m

所以,在 IEEE 754 binary64 里,当 53 个有效位均为整数部分时,lg(2532^{53}253-1) < 16,即它能表示的十进制将最多有 16 个有效数字。

2 ** 53 - 1; // 9007199254740991 ≈ 9e15 < 1e16
Math.log10(2 ** 53 - 1); // 15.954589770191003 < 16

3.2 小数部分

二进制的不同位数,能表示的十进制数列的差值。如下:

二进制
十进制差值
差值的范围

0.1

> 0.1 = 1e-1

0.01

> 0.1 = 1e-1

0.001

> 0.1 = 1e-1

0.0001

> 0.01 = 1e-2

0.00001

> 0.01 = 1e-2

0.000001

> 0.01 = 1e-2

0.0000001

> 0.001 = 1e-3

0.00000001

> 0.001 = 1e-3

0.000000001

> 0.001 = 1e-3

0.0000000001

> 0.0001 = 1e-4

从上表可以看出,二进制的 n 个有效位可以表示的十进制数列的差值的取值范围 10−m10^{-m}10−m < 2−n2^{-n}2−n,也就是说至少需要 m 个十进制有效位。最多需要几个呢?答案是 n 个,因为差值的小数点后有几位,能表示的十进制数列的有效数字最多就有几位。

所以,在 IEEE 754 binary64 里,当 53 个有效位均为小数部分时,2−532^{-53}2−53 ≈ -1.11e-16 > 10−1610^{-16}10−16,即它能表示的十进制小数最少要有 16 个有效数字。

2 ** -53; // 1.1102230246251565e-16

4. 总结

这部分内容虽然篇幅较长,但其实就解释了一句话,就是:读内存中的 IEEE 754 binary64 数值时,只要十进制的有效数字是 17 个就能保证“内存里存啥就显示啥”。用代码表述就是 toPrecision(17) 能显示出内存中的真实值,而 toPrecision(16) 能让小数“看起来”是对的。

Number(0.1); // 0.1
Number(0.1).toPrecision(16); // '0.1000000000000000'  = 0.1
Number(0.1).toPrecision(17); // '0.10000000000000001' > 0.1

Number(0.2); // 0.2
Number(0.2).toPrecision(16); // '0.2000000000000000' = 0.2
Number(0.2).toPrecision(17); // '0.20000000000000001' > 0.2

Number(1.005); // 1.005
Number(1.005).toPrecision(16); // '1.005000000000000' = 1.005
Number(1.005).toPrecision(17); // '1.0049999999999999' < 1.005

IEEE 754 binary64 里的 53 位有效数精度能提供十进制数的 15 到 17 个有效数精度。

  • 整数部分,有效位最多 16 位。253−12^{53}-1253−1 ≈ 9∗10159*10^{15}9∗1015 < 101610^{16}1016

  • 小数部分,有效位最少 16 位,保险 17 位。10−1610^{-16}10−16 < 2−532^{-53}2−53 ≈ 1.11e-16

中这样写道:

- 1 = 1

- 1 = 3

- 1 = 7

- 1 = 15

- 1 = 31

- 1 = 63

- 1 = 127

- 1 = 255

- 1 = 511

- 1 = 1023

- 1 = 2047

- 1 = 4095

- 1 = 8191

- 1 = 16383

- 1 = 32767

- 1 = 65535

- 1 = 131071

= 0.5 = 5e-1

= 0.25 = 2.5e-1

= 0.125 = 1.25e-1

= 0.0625 = 6.25e-2

= 0.03125 = 3.125e-2

= 0.015625 = 1.5625e-2

= 0.0078125 = 7.8125e-3

= 0.00390625 = 3.90625e-3

= 0.001953125 = 1.953125e-3

= 0.0009765625 = 9.765625e-4

而,需要再多 1 位有效数字来进行四舍五入,故小数部分需要 17 个有效数字。

212^121
222^222
232^323
242^424
252^525
262^626
272^727
282^828
292^929
2102^{10}210
2112^{11}211
2122^{12}212
2132^{13}213
2142^{14}214
2152^{15}215
2162^{16}216
2172^{17}217
2−12^{-1}2−1
2−22^{-2}2−2
2−32^{-3}2−3
2−42^{-4}2−4
2−52^{-5}2−5
2−62^{-6}2−6
2−72^{-7}2−7
2−82^{-8}2−8
2−92^{-9}2−9
2−102^{-10}2−10
维基百科
不连续的小数
双精度浮点数的格式和含义
在存储时是会损失精度