# 浮点数与IEEE 754

## 定义

二进制浮点数沿用科学计数法：先把数写成整数部分为一位的二进制小数，再乘以2的幂，即 $\pm(1.f)_2\times2^{e}$。IEEE 754把符号、指数、尾数三段装进定长字段，其中指数存成偏移后的 $E=e+127$（单精度），数值由下式还原：

$$
V=(-1)^S\times(1.f)_2\times 2^{E-127}.
$$

32位单精度的字段划分如下。

| 字段     |  位数 |    位置   | 含义               |
| :----- | :-: | :-----: | :--------------- |
| 符号 $S$ |  1  |   第31位  | 0为正，1为负          |
| 阶码 $E$ |  8  | 第30～23位 | 偏移后的指数，$E=e+127$ |
| 尾数 $f$ |  23 |  第22～0位 | 小数点后的部分，隐含前导1    |

写成位串时依次是`S EEEEEEEE FFF…F`，阶码在前、尾数在后。记尾数位为 $f_1f_2\cdots f_{23}$（$f_1$ 紧接小数点），则

$$
(1.f)_2=1+\sum_{i=1}^{23}f_i2^{-i},\qquad
V=(-1)^S\left(1+\sum_{i=1}^{23}f_i2^{-i}\right)2^{E-127}.
$$

这两个式子是换算的依据：先拆出三段，把阶码减127得到实际指数 $e$，再把尾数按 $2^{-1},2^{-2},\dots$ 展开，最后加上隐含的1。

## 性质

### 阶码用偏移表示

指数可能是负的，例如 $2^{-3}$ 表示很小的数。如果阶码直接存带符号数，比较两个浮点数的大小时就得先看阶码的符号位，硬件无法把位串当作无符号整数直接比较。IEEE 754给指数统一加一个偏移：单精度 $E=e+127$，双精度 $E=e+1023$。这样阶码字段本身就是无符号整数，指数的大小顺序与位串的字典序一致。

单精度阶码8位，规格化数对应 $E=1\sim254$，即 $e=-126\sim127$；双精度阶码11位，对应 $e=-1022\sim1023$。

### 隐藏的前导1

规格化之后整数部分总是1：整数部分若为0，左移一位、阶码加一即可；若大于1，则还没有规格化完。既然总是1，就不必存进尾数，省下一位。因此23位尾数实际提供24位有效数字，双精度的52位尾数提供53位。

### 精度与范围

| 格式  | 总位数 | 阶码位 | 尾数位 |  偏移  | 十进制有效数字 |
| :-- | :-: | :-: | :-: | :--: | :-----: |
| 单精度 |  32 |  8  |  23 |  127 |   约7位   |
| 双精度 |  64 |  11 |  52 | 1023 | 约15～16位 |

范围由阶码决定，精度由尾数决定。单精度能表示的最大规格化数约为 $3.4\times10^{38}$，双精度约为 $1.8\times10^{308}$。C中的`float`、`double`分别对应这两种格式。

> [!NOTE]
> 这里只讨论阶码既不全0也不全1的规格化数。阶码全0与全1另有约定，用来表示0、非规格化数、无穷大与NaN。

### 误差来源

十进制小数转二进制时，若最简分数的分母含有2以外的质因子，展开就无限循环。$0.1$ 的二进制是 $0.0001100110011\ldots_2$，规格化后为 $1.100110011\ldots_2\times2^{-4}$，尾数需要无限多位。23位尾数只能截断或就近舍入，存进去的值与 $\frac{1}{10}$ 相差约 $1.5\times10^{-9}$。

运算中还要对齐阶码、对结果舍入，误差会逐步累积。例如：

```python title="浮点误差" showLineNumbers=false
print(0.1 + 0.2)          # 0.30000000000000004
print(0.1 + 0.2 == 0.3)   # False
```

第二行输出`False`，说明两个结果不相等。比较浮点数应写成 $|a-b|<\varepsilon$ 的形式，而不是直接用等号。

## 例子

**解码`0 10000010 101000…0`**

1. **拆字段**：$S=0$；$E=(10000010)_2=130$；尾数前三位是 $101$，其余为0。

2. **定符号**：$S=0$，是正数。

3. **算指数**：$e=130-127=3$。

4. **算尾数**：$0.101_2=2^{-1}+2^{-3}=0.5+0.125=0.625$，加上隐含的1得 $1.625$。

5. **合成**：$V=1.625\times2^3=1.625\times8=13.0$。

**为什么 $0.1+0.2$ 不等于 $0.3$**

$0.1$、$0.2$、$0.3$ 在二进制下都是无限循环小数，存入时各自被舍入成最接近的可表示值。三个值都不等于原来的十进制小数，相加的结果自然与 $0.3$ 的存储值不同，差额在双精度下约为 $10^{-17}$ 量级。

这不是浮点数算错了，而是舍入的必然结果。把判断改成 $|(0.1+0.2)-0.3|<\varepsilon$ 后，比较就能通过。

## 练习

1. 位串`0 10000001 100000…0`表示的十进制值是 \_\_\_\_\_\_（decoded）。

   答案：6

   $E=129$，实际指数 $129-127=2$；尾数 $0.1_2=0.5$，加上隐含的1得 $1.5$。$1.5\times2^2=6$。

2. 填出IEEE 754各字段的位数。

   1. 单精度的阶码占 \_\_\_\_\_\_（single-exp） 位。

      答案：8

      单精度共32位：符号1位、阶码8位、尾数23位。

   2. 单精度的尾数占 \_\_\_\_\_\_（single-man） 位。

      答案：23

      尾数不存储隐含的前导1，所以只占23位。

   3. 双精度的尾数占 \_\_\_\_\_\_（double-man） 位。

      答案：52

      双精度共64位：符号1位、阶码11位、尾数52位。

3. $8.0$ 的单精度表示中，阶码字段的8位二进制是 \_\_\_\_\_\_（exponent-bits）（只填0和1）。

   答案：10000010

   $8.0=1.0_2\times2^3$，实际指数 $e=3$，阶码 $E=3+127=130=(10000010)_2$。尾数位全为0。

4. 在浮点数中 $0.1+0.2\ne0.3$ 的原因是：

   * A.  尾数位数有限，$0.1$ 这类小数在二进制中无限循环，只能舍入
   * B.  阶码溢出，结果被截断成更小的数
   * C.  两个数的符号位不同，加法退化成了减法
   * D.  单精度无法表示 $0.3$，只能存成0

   答案：A

   $0.1$、$0.2$、$0.3$ 的二进制展开都无限循环，存入时已经各自舍入；加法还要再舍入一次，结果与 $0.3$ 的存储值不相等。这与溢出无关，也不是符号位造成的。

## 交互探索

交互演示：0.1在32位里是什么样？。输入一个十进制数，拆开它的IEEE 754单精度符号、阶码和尾数。 请在站点操作演示。

> [!NOTE]
> 观察什么
>
> 输入 $0.1$ 时尾数已经被舍入；改变数值时，注意符号位、阶码和尾数分别怎样变化，以及还原值与原数的差。
