> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/cs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/cs/floating-point/read.md).

# 3. 真实的值（读时）

理解了[双精度浮点数的格式和含义](/cs/floating-point/binary64.md)以及它[在存储时是会损失精度](/cs/floating-point/write.md)的，在此基础上，我们继续探索下编程语言是如何读取并显示内存中的 binary64 数据的，以 JavaScript 为例。

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

![](https://user-images.githubusercontent.com/7055639/143994586-71349785-033a-4add-adb5-e0c7f72c0bb9.jpeg)

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

```javascript
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. 截取精度的规则

[维基百科](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)中这样写道：

> The 53-bit significand precision gives from 15 to 17 significant decimal digits precision ($$2^{−53}$$ ≈ 1.11 × $$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 个就能保证不出错（内存里存啥就显示啥）。

来看几个例子感受下。

```javascript
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`：能表示 $$2^2$$ = 4 个十进制数，它们之间的差值是 1。如下：

| 二进制  | 十进制 |
| ---- | --- |
| 00   | 0   |
| 01   | 1   |
| 10\* | 2\* |
| 11\* | 3\* |

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

#### 2.1.3 有 3 个二进制有效位

当有 3 个二进制有效位时 `xxx`：能表示 $$2^3$$ = 8 个十进制数，它们之间的差值是 1。如下：

| 二进制   | 十进制 |
| ----- | --- |
| 000   | 0   |
| 001   | 1   |
| 010   | 2   |
| 011   | 3   |
| 100\* | 4\* |
| 101\* | 5\* |
| 110\* | 6\* |
| 111\* | 7\* |

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

#### 2.1.4 小结

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

1. 可以表示 $$2^n$$ 个十进制数，且它们是个等差数列，差值是 1
2. 每增加 1 个二进制有效位，就会新增 $$2^n-1$$ 个十进制数（即高位为 1 的数值）

### 2.2 纯小数

#### 2.2.1 有 1 个二进制有效位

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

| 二进制 | 十进制 |
| --- | --- |
| 0.0 | 0   |
| 0.1 | 0.5 |

#### 2.2.2 有 2 个二进制有效位

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

| 二进制    | 十进制    |
| ------ | ------ |
| 0.00   | 0      |
| 0.01\* | 0.25\* |
| 0.10   | 0.5    |
| 0.11\* | 0.75\* |

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

#### 2.2.3 有 3 个二进制有效位

当有 3 个二进制有效位时 `0.xxx`：能表示 $$2^3$$ = 8 个十进制数，它们之间的差值是 $$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)}$$ = $$2^2$$ 个，其末位均是 $$1\_{(2)}$$。

#### 2.2.4 有 4 个二进制有效位

当有 4 个二进制有效位时 `0.xxxx`：能表示 $$2^4$$ = 16 个十进制数，它们之间的差值是 $$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)}$$ = $$2^3$$ 个，其末位均是 $$1\_{(2)}$$。

#### 2.2.5 小结

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

1. 可以表示 $$2^n$$ 个十进制数，且它们是个等差数列，差值是 $$2^{-n}$$，数列范围是 \[$$2^{-n}$$, 1)
   * 与纯整数不同，二进制只能精准地表示一部分十进制小数 $$\frac{k}{2^n}$$，其中 0 ≤ k < $$2^n$$ - 1，k 是整数
   * 而其它小数只能是一个无限逼近真实值的二进制值，随着 n 越来越大时
2. 每增加 1 个二进制有效位，就会新增 $$2^{n-1}$$ 个十进制数（即末位为 1 的数值）

### 2.3 小结

当有 n 个二进制有效位（可能同时包含整数部分和小数部分）时：

1. 可以表示 $$2^n$$ 个十进制数，且会形成一个等差数列
   * 整数部分的差值是 1
   * 小数部分的差值是 $$2^{-m}$$，小数将是“不连续”的（相对整数而言）
2. 每增加 1 个二进制有效位，就会新增 $$2^{n-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                 | $$2^1$$ - 1 = 1         | < 1e1                 |
| 11                | $$2^2$$ - 1 = 3         | < 1e1                 |
| 111               | $$2^3$$ - 1 = 7         | < 1e1                 |
| 1111              | $$2^4$$ - 1 = 15        | <p>< 1e1<br>< 1e2</p> |
| 11111             | $$2^5$$ - 1 = 31        | < 1e2                 |
| 111111            | $$2^6$$ - 1 = 63        | < 1e2                 |
| 1111111           | $$2^7$$ - 1 = 127       | <p>< 1e2<br>< 1e3</p> |
| 11111111          | $$2^8$$ - 1 = 255       | < 1e3                 |
| 111111111         | $$2^9$$ - 1 = 511       | < 1e3                 |
| 1111111111        | $$2^{10}$$ - 1 = 1023   | <p>< 1e3<br>< 1e4</p> |
| 11111111111       | $$2^{11}$$ - 1 = 2047   | < 1e4                 |
| 111111111111      | $$2^{12}$$ - 1 = 4095   | < 1e4                 |
| 1111111111111     | $$2^{13}$$ - 1 = 8191   | < 1e4                 |
| 11111111111111    | $$2^{14}$$ - 1 = 16383  | <p>< 1e4<br>< 1e5</p> |
| 111111111111111   | $$2^{15}$$ - 1 = 32767  | < 1e5                 |
| 1111111111111111  | $$2^{16}$$ - 1 = 65535  | < 1e5                 |
| 11111111111111111 | $$2^{17}$$ - 1 = 131071 | <p>< 1e5<br>< 1e6</p> |

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

$$10^{m-1}$$ < $$2^n$$ - 1 < $$10^m$$，即 m-1 < lg($$2^n$$ - 1) < m

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

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

### 3.2 小数部分

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

<table><thead><tr><th width="213.33333333333331">二进制</th><th width="326.9130434782609">十进制差值</th><th>差值的范围</th></tr></thead><tbody><tr><td>0.1</td><td><span class="math">2^{-1}</span> = 0.5 = 5e-1</td><td>> 0.1 = 1e-1</td></tr><tr><td>0.01</td><td><span class="math">2^{-2}</span> = 0.25 = 2.5e-1</td><td>> 0.1 = 1e-1</td></tr><tr><td>0.001</td><td><span class="math">2^{-3}</span> = 0.125 = 1.25e-1</td><td>> 0.1 = 1e-1</td></tr><tr><td>0.0001</td><td><span class="math">2^{-4}</span> = 0.0625 = 6.25e-2</td><td>> 0.01 = 1e-2</td></tr><tr><td>0.00001</td><td><span class="math">2^{-5}</span> = 0.03125 = 3.125e-2</td><td>> 0.01 = 1e-2</td></tr><tr><td>0.000001</td><td><span class="math">2^{-6}</span> = 0.015625 = 1.5625e-2</td><td>> 0.01 = 1e-2</td></tr><tr><td>0.0000001</td><td><span class="math">2^{-7}</span> = 0.0078125 = 7.8125e-3</td><td>> 0.001 = 1e-3</td></tr><tr><td>0.00000001</td><td><span class="math">2^{-8}</span> = 0.00390625 = 3.90625e-3</td><td>> 0.001 = 1e-3</td></tr><tr><td>0.000000001</td><td><span class="math">2^{-9}</span> = 0.001953125 = 1.953125e-3</td><td>> 0.001 = 1e-3</td></tr><tr><td>0.0000000001</td><td><span class="math">2^{-10}</span> = 0.0009765625 = 9.765625e-4</td><td>> 0.0001 = 1e-4</td></tr></tbody></table>

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

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

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

而[不连续的小数](#2.2-chun-xiao-shu)，需要再多 1 位有效数字来进行四舍五入，故小数部分需要 17 个有效数字。

## 4. 总结

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

```javascript
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 位。$$2^{53}-1$$ ≈ $$9\*10^{15}$$ < $$10^{16}$$
* 小数部分，有效位最少 16 位，保险 17 位。$$10^{-16}$$ < $$2^{-53}$$ ≈ 1.11e-16
