浮点数的基本思想

定点数的小数点位置固定,表示范围受位数直接限制。浮点数把一个数拆成三部分:

$$
(-1)^S \times M \times 2^E
$$

部分 含义 表现形式
$S$ 符号位,决定正负, 取值为0/1
$M$ 尾数或有效数,决定精度 非负的定点小数
$E$ 阶码,决定数量级 定点整数,常用移码表示

IEEE 754 标准

IEEE 754 规定了浮点数的编码格式、特殊值、舍入规则和异常状态。

浮点数编码格式如下。

1000

格式 总位数 符号位 阶码位 尾数字段 阶码偏置
单精度 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

NaN 表示“不是一个数”,常来自未定义或无意义的运算,例如 $0/0$、$\infty-\infty$。

## 表示范围

1000

数轴从左到右可以分成:

  1. 负上溢:结果小于最小负规格化数。
  2. 规格化负数:负的普通浮点数。
  3. 非规格化负数:非常接近 0 的负数。
  4. 负下溢:负数小到无法保持规格化,进入非规格化数或 $-0$。
  5. 0:包括 +0-0
  6. 正下溢:正数小到无法保持规格化,进入非规格化数或 +0
  7. 非规格化正数:非常接近 0 的正数。
  8. 规格化正数:正的普通浮点数。
  9. 正上溢:结果大于最大正规格化数。

浮点数加减运算

浮点加减流程是:

  1. 比较两个操作数的阶码。并大减小得阶码差
  2. 对阶:根据阶码差,把阶码较小的数的尾数连同隐藏位右移。连同隐藏位的尾数每右移一位,阶码就增加1。最终使两个阶码相同。
  3. 算上符号、算上隐藏位的尾数相加或相减。
  4. 规格化:调整尾数和阶码,使结果回到合法格式。
  5. 舍入:把超出尾数字段的位按舍入规则处理。
  6. 检查溢出。
对阶会丢失低位

阶码较小的数右移时,低位可能被移出尾数字段。若两个数数量级差距太大,小数可能完全消失,这就是“大数吃小数”。

## 舍入

浮点数的尾数字段有限,很多实数不能精确表示。例如十进制的 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
2
3
a =  1.0e20
b = -1.0e20
c = 1.0

若先算左边:

1
2
3
4
(a + b) + c
= (1.0e20 - 1.0e20) + 1.0
= 0.0 + 1.0
= 1.0

若先算右边:

1
2
a + (b + c)
= 1.0e20 + (-1.0e20 + 1.0)

其中 -1.0e20 + 1.0 会发生“大数吃小数”,1.0 在对阶时被丢掉:

1
-1.0e20 + 1.0 ≈ -1.0e20

所以:

1
a + (b + c) ≈ 0.0

两种括号顺序得到的结果不同:

1
2
(a + b) + c = 1.0
a + (b + c) = 0.0

这说明浮点加法的计算结果依赖运算顺序。并行求和、编译器优化、表达式重排都会受到这一点影响。