Floating Point Numbers
浮点数的基本思想
定点数的小数点位置固定,表示范围受位数直接限制。浮点数把一个数拆成三部分:
$$
(-1)^S \times M \times 2^E
$$
| 部分 | 含义 | 表现形式 |
|---|---|---|
| $S$ | 符号位,决定正负, | 取值为0/1 |
| $M$ | 尾数或有效数,决定精度 | 非负的定点小数 |
| $E$ | 阶码,决定数量级 | 定点整数,常用移码表示 |
IEEE 754 标准
IEEE 754 规定了浮点数的编码格式、特殊值、舍入规则和异常状态。
浮点数编码格式如下。
| 格式 | 总位数 | 符号位 | 阶码位 | 尾数字段 | 阶码偏置 |
|---|---|---|---|---|---|
单精度 float |
32 | 1 | 8 | 23 | 127 |
双精度 double |
64 | 1 | 11 | 52 | 1023 |
规格化数
当阶码字段既不全 0,也不全 1 时,表示规格化数:
$$
(-1)^S \times (1.F) \times 2^{e-bias}
$$
这里的 1.F 表示尾数最高位的 1 不存入字段,称为隐藏位。
非规格化数
当阶码字段全 0、尾数字段不全 0 时,表示非规格化数:
$$
(-1)^S \times (0.F) \times 2^{1-bias}
$$
非规格化数没有隐藏位 1。它用于表示非常接近 0 的数,让数值可以逐渐下溢,而不是突然从最小规格化数跳到 0。
各部分取值的含义
| 阶码字段 | 尾数字段 | 含义 |
|---|---|---|
| 全 0 | 全 0 | $\pm 0$ |
| 全 0 | 非 0 | 非规格化数 |
| 非全 0 且非全 1 | 任意 | 规格化数 |
| 全 1 | 全 0 | $\pm \infty$ |
| 全 1 | 非 0 | NaN |
NaN 表示“不是一个数”,常来自未定义或无意义的运算,例如 $0/0$、$\infty-\infty$。
数轴从左到右可以分成:
- 负上溢:结果小于最小负规格化数。
- 规格化负数:负的普通浮点数。
- 非规格化负数:非常接近 0 的负数。
- 负下溢:负数小到无法保持规格化,进入非规格化数或 $-0$。
- 0:包括
+0和-0。 - 正下溢:正数小到无法保持规格化,进入非规格化数或
+0。 - 非规格化正数:非常接近 0 的正数。
- 规格化正数:正的普通浮点数。
- 正上溢:结果大于最大正规格化数。
浮点数加减运算
浮点加减流程是:
- 比较两个操作数的阶码。并大减小得阶码差
- 对阶:根据阶码差,把阶码较小的数的尾数连同隐藏位右移。连同隐藏位的尾数每右移一位,阶码就增加1。最终使两个阶码相同。
- 算上符号、算上隐藏位的尾数相加或相减。
- 规格化:调整尾数和阶码,使结果回到合法格式。
- 舍入:把超出尾数字段的位按舍入规则处理。
- 检查溢出。
阶码较小的数右移时,低位可能被移出尾数字段。若两个数数量级差距太大,小数可能完全消失,这就是“大数吃小数”。
浮点数的尾数字段有限,很多实数不能精确表示。例如十进制的 0.1 在二进制中是无限循环小数。
当结果需要更多尾数位时,机器必须舍入,选择一个可表示的近似值。
为了使得舍入后的结果尽量精准,IEEE 754引入3辅助位:
| 辅助位 | 定义 |
|---|---|
| Guard bit | 被保留尾数右侧的第一位 |
| Round bit | Guard bit 右侧的一位 |
| Sticky bit | 更低位是否出现过 1,只要有一个被移出的低位为 1,Sticky bit 就为 1 |
这些辅助位组成3bit数$M$。
常见舍入方向包括:
| 舍入方向 | 含义 |
|---|---|
| 就近舍入 | 取最近的可表示数 |
| 截断 | 直接截去多余部分 |
| 向 $+\infty$ 舍入 | 向正无穷方向取可表示数 |
| 向 $-\infty$ 舍入 | 向负无穷方向取可表示数 |
IEEE 754 默认舍入方式通常是就近舍入, ties to even:将M附在尾数$f$后面使尾数成为$f’$,选择距离$f’$最近的可表示数;若正好在两个可表示数中间,选择尾数最低有效位为偶数的那个。
即:看M值,四舍六入五凑偶
溢出
浮点数的阶码也有限,因此结果可能超出可表示范围。
| 情况 | 含义 | 典型结果 |
|---|---|---|
| 上溢 | 结果绝对值太大,阶码装不下 | $\pm\infty$ 或最大有限数 |
| 下溢 | 结果绝对值太小,接近 0 | 非规格化数或 $\pm 0$ |
整数溢出常表现为截断或环绕;浮点上溢通常进入无穷大或最大有限值,具体取决于舍入模式。
对阶还会导致“大数吃小数”现象。
假设有一个很大的数 $A$ 和一个很小的数 $B$:
$$
|A| \gg |B|
$$
计算 $A+B$ 时,必须把 $B$ 的尾数右移到和 $A$ 同一阶码。若右移位数超过尾数能保留的精度,$B$ 的有效位会全部移出,结果就变成:
$$
A+B=A
$$
这是浮点表示精度不足导致的计算结果。
在有限精度下,1.0e20 + 1.0 可能仍得到 1.0e20。因为 1.0 在对阶时太小,无法影响 1.0e20 的有效位。
数学中的加减法满足结合律:
$$
(a+b)+c=a+(b+c)
$$
但浮点加减法每做一次运算都可能发生一次对阶、舍入和低位丢失,因此一般不满足结合律。
例如:
1 | a = 1.0e20 |
若先算左边:
1 | (a + b) + c |
若先算右边:
1 | a + (b + c) |
其中 -1.0e20 + 1.0 会发生“大数吃小数”,1.0 在对阶时被丢掉:
1 | -1.0e20 + 1.0 ≈ -1.0e20 |
所以:
1 | a + (b + c) ≈ 0.0 |
两种括号顺序得到的结果不同:
1 | (a + b) + c = 1.0 |
这说明浮点加法的计算结果依赖运算顺序。并行求和、编译器优化、表达式重排都会受到这一点影响。