浮点数表示

需掌握 32 位和 64 位浮点数的 IEEE 表示,可能在选择题中考察。

实数的二进制表示

实数在计算机中的存储遵循 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(尾数) 三个部分存储:

s
exp
frac
s
exp
frac (51:32)
31
30
23
22
0
frac (31:0)
63
62
52
51
32
Single Precision
Double Precision
31
0
尾数 23 位
阶码 8 位
尾数 52 位
阶码 11 位
  • $1$ 位的 $s$ 与上述计算公式中的符号位 $s$ 相同,位于浮点数二进制表示的最高位,标志了浮点数的正负,如果 $s = 1$ 的话 $V$ 是负数,如果 $s = 0$ 的话 $V$ 为正数
  • $k$ 位的 $exp = e_{k-1} \cdots e_1 e_0$ 用于计算指数部分 $E$,使用原码表示,其中 $E = exp - Bias$,其中 $Bias = 2^{k-1} - 1$
    • 对于单精度浮点数,$k = 8$,$Bias = 2^{8-1} - 1 = 127$,$1 \le exp \le 254$,$-126 \le M \le 127$
    • 对于双精度浮点数,$k = 11$,$Bias = 2^{11-1} - 1 = 1023$, $1 \le exp \le 1022$,$-1022 \le M \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总位数偏置值
单精度182332127
双精度11152641023

单精度浮点数表示为:

$$(-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。

下图是浮点数各种类型的图示(以单精度浮点数为例):

s
1
1
1
1
1
1
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
s
1
1
1
1
1
1
1
1
≠ 0
s
0
0
0
0
0
0
0
0
frac
s
≠ 0 and ≠ 255
frac
Normalized
Denormalized
Infinity
NaN
  • 非正常值(Denormalized)的阶码全为 0
  • 无穷大(Infinity)的阶码全为 1,小数位为 0
  • 非数字(NaN,Not a Number)的阶码全为 1,小数位不为 0

字面值转二进制

前文已经提到如果我们有浮点数的 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/8
2/8
3/8
5/8
5/8
6/8
7/8
1

如果一个尾数的大小与这些点都不相同的话,则需要找一个临近的点来近似,这也是导致精度丢失的原因:尾数的二进制表示法无法精确地表示 [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. 对阶:为了进行加减运算,两个浮点数必须具有相同的指数,这里采用低阶向高阶对齐的原则,过程如下:
    • 比较两个浮点数的指数。
    • 将较小指数的浮点数的尾数右移,直到两个指数相等。
    • 右移过程中,需要注意尾数的精度损失(尾数右移时可能会丢失低位精度)。
  2. 尾数加减:在指数对齐后,直接对两个浮点数的尾数进行加减。
    • 由于尾数已经对齐,可以直接进行加减操作。
    • 根据操作结果可能需要处理进位或借位。
  3. 尾数规格化:确保结果符合标准化浮点数的格式,即尾数以 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$ 时,使用以下步骤:

  1. 对阶:低位向高位,$B = 0.0111_{2} \times 2^3$。
  2. 尾数相加:$A + B = (1.101_{2} + 0.0111_{2}) × 2^3 = 10.0001_{2} × 2^3$
  3. 尾数规格化:$10.0001_{2} × 2^3 = 1.00001 × 2^4$