# 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://anjia1.gitbook.io/cs/floating-point/read.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
