浮点数表示
实数的二进制表示
实数在计算机中的存储遵循 IEEE 浮点数标准,但在这里为了方便理解 IEEE 标准,这里首先阐明一般 实数 在计算机中是如何存储的。
在这里 实数 分为两个部分存储:整数部分 和 小数部分,两个部分在逻辑上用 ·
隔开。
比如对于以下 浮点数 的二进制表示:
$$d_m d_{m-1} \cdots d_1 d_0 \cdot d_{-1} d_{-2} \cdots d_{-n}$$
其中每一位对应的数值如下表所示:
$d_m$ | $d_{m-1}$ | $\cdots$ | $d_1$ | $d_0$ | $d_{-1}$ | $d_{-2}$ | $\cdots$ | $d_{-n}$ |
---|---|---|---|---|---|---|---|---|
$2^m$ | $2^{m-1}$ | $\cdots$ | $2$ | $1$ | $1/2$ | $1/4$ | $\cdots$ | $1/2^{n}$ |
上述二进制表示对应的数值为
$$b = \sum_{i=-n}^{m} {2^{i} \times b_{i}}$$
其中 $b_i = 0$ 或 $b_i = 1$,表示第 i 为是 0 还是 1。
举例说明如何使用上述表示法计算 实数:
- $101.11_{2}$ 对应的数值为 $1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2^{-2} = 5.75$
- $1011.1_{2}$ 对应的数值为 $8 + 0 + 2 + 1 + \frac{1}{2} = 11.5$
字面值转二进制
举个实际的例子,将 1.2 转换为二进制表示。
整数部分 1 的二进制是 1。
小数部分 0.2 的二进制表示是一个无限循环小数。通过不断乘以 2,可以得到近似的二进制:
- 0.2 × 2 = 0.4 → 整数部分 0
- 0.4 × 2 = 0.8 → 整数部分 0
- 0.8 × 2 = 1.6 → 整数部分 1
- 0.6 × 2 = 1.2 → 整数部分 1
- 0.2 × 2 = 0.4 → 重复…
于是,1.2 在二进制中近似为 1.0011001100110011...
。
IEEE 浮点数表示
上述 浮点数 存储方式的缺点在于如果要表示比较大的数,就需要比较多的二进制位数,比如对于 $5 \times 2^{100}$ 就需要 103 位。IEEE 表示 就解决了这个问题,在 IEEE 表示 中,浮点数 $V$ 被表示为 $V = (-1)^{s} \times M \times 2^{E}$,其中 $s, M, E$ 的含义如下:
- $s$ 为 符号位
- $M$ 为 乘法因子
- $E$ 为 指数部分
IEEE 754 标准 是定义浮点数表示和算术的国际标准,它定义了多种不同精度的浮点数格式,但最常见的是 单精度 single precesion(32 位)和 双精度 double precesion(64 位),浮点数分为 s
(符号位)、exp
(阶码)、frac
(尾数)三个部分存储:
$1$ 位的 $s$ 与上述计算公式中的 符号位 $s$ 相同,位于浮点数二进制表示的 最高位,标志了浮点数的正负,如果 $s = 1$ 的话 $V$ 是负数,如果 $s = 0$ 的话 $V$ 为正数
$k$ 位的 $exp = e_{k-1} \cdots e_1 e_0$ 用于计算 指数部分 $E$($exp$ 为 unsigned 值,但不能为全 0 或全 1),其中 $E = exp - Bias$,$Bias = 2^{k-1} - 1$
- 对于 单精度 浮点数
- $k = 8$,$Bias = 2^{8-1} - 1 = \textbf{127}$
- $1 \le exp \le 254$
- $-126 \le E \le 127$
- 对于 双精度 浮点数
- $k = 11$,$Bias = 2^{11-1} - 1 = \textbf{1023}$
- $1 \le exp \le 1022$
- $-1022 \le E \le 1023$
- 对于 单精度 浮点数
$n$ 位的 $frac = 0 \cdot f_{n-1} \cdots f_1 f_0$ 用于计算 因子 $M$,其中 $M = 1 + frac$,$frac$ 表示的小数计算方式见 实数的二进制表示
- 对于 单精度 浮点数,$n = 23$,$M$ 的最大值为 $2 - 2^{-23}$,$M$ 最小值为 $1$
- 对于 双精度 浮点数,$n = 52$,$M$ 的最大值为 $2 - 2^{-52}$,$M$ 最小值为 $1$
总结 单精度 和 双精度 浮点数 表示如下:
类型 | 符号位 s | 阶码 exp | 尾数 frac | 总位数 | 偏置值 |
---|---|---|---|---|---|
单精度 | 1 | 8 | 23 | 32 | 127 |
双精度 | 1 | 11 | 52 | 64 | 1023 |
单精度 浮点数表示为:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - 127}$$
双精度 浮点数表示为:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - 1023}$$
异常值
注意 IEEE 浮点数 表示分为 Normalized Values(正常值)和 Denormalized Values(非正常值)以及特殊值,上节中提到的 IEEE 浮点数 计算方法只适用于 正常值,
正常值 的 阶码(exp)不能为全 0 或全 1。
下图是 浮点数 各种类型的图示(以 单精度 为例):
- 非正常值(Denormalized)的 阶码全为 0
- 无穷大(Infinity)的 阶码全为 1,尾数位为 0
- 非数字(NaN,Not a Number)的 阶码全为 1,尾数位不为 0
需要注意的是 非正常值 的 浮点数 计算公式 与 正常值 不同:
$$\text{value} = (-1)^{\text{sign}} \times 0.f \times 2^{1 - \text{bias}}$$
注意这里的:
- 没有隐含的 “1”,因为它是 非正规数。
- 指数固定为 $1 - \text{bias}$(而不是 $e - \text{bias}$,因为 阶码是全 0)
- 这是为了在非常接近 0 的地方保持精度连续性(即从最小正规数向 0 过渡的“填充区”)
下表总结了给出了 浮点数 表示各种情况的有效值计算:
类型 | 阶码(exp) | 尾数(fraction) | 有效值公式 |
---|---|---|---|
正常值 | 非全 0、非全 1 | $f$ | $± 1.f \times 2^{e - \text{bias}}$ |
非正常值 | 全 0 | $f$(必须非 0,否则是 $0$) | $± 0.f \times 2^{1 - \text{bias}}$ |
$± 0$ | 全 0 | 全 0 | $± 0$ |
$±\infty$ | 全 1 | 全 0 | $± \infty$ |
NaN | 全 1 | 非 0 | Not a Number(结果非法或未定义) |
字面值转二进制
前文已经提到如果我们有 浮点数 的 IEEE 二进制表示,如何将其转化为实际的 浮点数,就是通过如下的公式:
$$(-1)^s \times 1.\text{frac} \times 2^{\text{exp} - \text{bias}} $$
还有一种常见的考题是给定我们一个 浮点数字面值,比如 2.25,然后让我们去反推它的二进制表示,这里有没有什么简便的解法呢?
最简便的方法还是反向转换,即将一个 浮点数 表示为 一点几几($1.\text{frac}$)乘以二的多少次方($2^{n}$)的格式,有这两部分可以分别计算出 阶码和尾数,然后符号位由数字的正负判断。
以 2.25 为例,如果我们想得到该 浮点数 的 单精度 表示:
$$2.25 = 1.125 \times 2^{1} = \frac{9}{8} \times 2^1 = (1 + \frac{1}{8}) \times 2^1 = (1 + 2^{-3}) \times 2^1$$
由此可以计算出 浮点数 的各个部分:
- 符号位 为 0
- 阶码 为 $1 + 127 = 128$,二进制表示为
1000 0000B
- 尾数 为
0010 0000 0000 ....
所以 浮点数 的二进制为 0100 0000 0001 0000 ...
,十六进制为 40100000H。
一般而言,试题只会考察这种没有精度损失的 浮点数二进制转换,对于有精度损失的情况,由于比较繁琐,基本不会考察,其 尾数 部分的计算与 一般实数字面值转二进制 相同。
表示精度
上述的例子是一个比较理想的例子,2.25 可以精确地用 浮点数 表示。但在真实场景中,很多 实数 都是无法精确地用 浮点数 表示的。比如 1.2 这个数:
$$1.2 = (1 + \frac{1}{5}) \times 2^1$$
其中 $\frac{1}{5}$ 无法表示为若干个 $\frac{1}{2^n}$ 之和,所以对于这种情况,我们只能尽量地去接近这个数。
若要理解如何接近这个数,我们首先要理解 精度 的概念。这里我们首先从简单的例子出发,假设 阶码只有 3 位,那么这些 尾数可以被精确表示:
$$\sum_{i=0}^{3} f \times 2^i, f \in {0, 1}$$
在数轴中对应 [0, 1] 区间中的 8 个点:
如果一个尾数的大小与这些点都不相同的话,则需要找一个临近的点来近似,这也是导致 精度 丢失的原因:尾数的二进制表示法无法精确地表示 [0, 1] 中的每一个 实数,对于无法精确表示的,只能去近似。
但是这种误差可以随着 尾数 位数的增加而不断减小,比如对于 单精度浮点数 表示,尾数(frac)为 23 位,可以精确表示以下这些小数:
$$0, \frac{1}{2^{23}}, \frac{2}{2^{23}}, \frac{3}{2^{23}}, \cdots, \frac{2^{23} - 1}{2^{23}}, 1$$
双精度浮点数 表示,尾数为 52 位,可以精确表示以下这些小数:
$$0, \frac{1}{2^{52}}, \frac{2}{2^{52}}, \frac{3}{2^{52}}, \cdots, \frac{2^{52} - 1}{2^{52}}, 1$$
所以 尾数位数越多,精度越高,用以近似表示某些 实数 时,误差更小。
舍入 是在数值计算中将一个数字转换为特定精度的过程。由于计算机中的 浮点数 表示有限,许多数学运算结果不能完全精确地用 浮点数 表示,因此需要 舍入 来逼近这些结果。
浮点数加减
浮点数 加减计算过程包含以下几个步骤:对阶、尾数加减、尾数规格化。
- 对阶:为了进行加减运算,两个 浮点数 必须具有相同的指数,这里采用低阶向高阶对齐的原则,过程如下:
- 比较两个浮点数的 指数。
- 将较小指数的浮点数的 尾数右移,直到两个指数相等。
- 右移过程中,需要注意尾数的 精度损失(尾数右移时可能会丢失低位精度)。
- 尾数加减:在指数对齐后,直接对两个 浮点数的 尾数进行加减。
- 由于尾数已经对齐,可以直接进行 加减操作。
- 根据操作结果可能需要处理进位或借位。
- 尾数规格化:确保结果符合标准化浮点数的格式,即尾数以 1 开头。
- 如果结果尾数不符合 1.xxxx 格式,则需要进行 规格化调整。
- 左移尾数并相应减少指数,或右移尾数并相应增加指数。
- 规格化后,可能还需要进行 舍入,以符合尾数的位数限制。
- 根据舍入模式(如“向偶数舍入”、“向零舍入”等)完成必要操作。
在 对阶 和 尾数规格化 的过程中,由于可能存在 尾数右移,所以可能会导致 精度缺失。
因为 IEEE 浮点数尾数的位数是有限的,如果右移的过程中尾数中最右边的 1 被清除,就会导致 精度缺失。
举一个实际的例子来说明一下,假设 $A = 1.625 × 2^3 = 1.101_{2} × 2^3$,$B = 1.75 × 2^1 = 1.11_{2} × 2^1$,在计算 $A+B$ 时,使用以下步骤:
- 对阶:低位向高位,$B = 0.0111_{2} \times 2^3$。
- 尾数相加:$A + B = (1.101_{2} + 0.0111_{2}) × 2^3 = 10.0001_{2} × 2^3$
- 尾数规格化:$10.0001_{2} × 2^3 = 1.00001 × 2^4$