浮点数
浮点数的名称中就蕴含了“小数点移动”的意义。上一篇我们提到,浮点数的表示方式类似科学计数法,即写成一个实数 $a$ 乘以一个基数 $r$ 的 $E$ 次方,即 $a \times r^E$。基数 $r$ 越大,$r^E$ 就越大,表示的范围就越大,而精度则会下降。在计算机中,常用的基数 $r$ 为 2。因此下文都以基数为 2 展开。我们将次数 $E$ 称为阶码,实数 $a$ 称为尾数。那么,二进制浮点数的真值表示可以形如 $([-0.1101010000]_2) \times 2^{[-11]_2} = -\frac{53}{512}$。
另外,我们知道十进制数转换成二进制小数时可能会产生无限循环小数,只能保留精度要求的位数,造成精度缺失,那么浮点数即使在它能表示的大小范围内,也 无法表示所有的小数。可以理解为浮点数的值域是实数轴上挖去了很多的小范围。
二进制小数的每一位都表示 2 的某个次方数,如 $2^{-1} = 0.5$,$2^{-2} = 0.25$。那么用两位二进制小数能表示的只有 $0,0.5,0.25,0.75$,中间的数都是缺失的,只能用更多的位数去逼近要表示的值,理论上取极限时可以得到任何数,但我们并没有无穷多的位数。
浮点数的机器码
我们知道,阶码和尾数都可以是负数,因此它们都需要符号位。一种容易想到的表示方式是:取出一部份二进制位作为阶码,另一部份作为尾数。例如:$[-0.1101010000]_2) \times 2^{[-11]_2} \rightarrow 1,0011;1.1101010000$。此处,阶码和尾数都使用原码表示,问题是:阶码和尾数分别使用什么编码比较合适?考虑浮点数的运算。对于加减来说,如果两个数的阶码相同,那么尾数直接加减;对于乘除来说,阶码相加减,尾数做乘除法。此外,浮点数之间还需要比较大小。因此,阶码使用移码 比较合适,且移码不需要关心符号位。而对于尾数,原码的值更直观,做乘除法更简便,且更易于规格化,所以 尾数使用原码。
浮点数的规格化
按照上述的表示方式,一个浮点数可以有无数种表示方式。这就好比 $3.14 \times 10^2 = 0.314 \times 10^3 = 31.4 \times 10^1$,只需要挪动小数点位置就有一种新的表示。为了统一浮点数的表示,增加数据的表示精度,我们需要对浮点数进行规格化。具体来说,就是通过改变阶码(小数点位置)来让尾数数值部份的最高位为有效位。这种表示方式与十进制数的科学计数法也十分类似,我们会通过调整次数 $E$ 将实数 $a$ 写成 $1\leq |a| \lt 10$ 的一个值,那么最高位个位就是一个有效值。通过规格化,我们可以让尾数部份尽可能表示更多的小数位数,也就是提高精度。
我们将规格化时,小数点向左移动(即阶码减少)称为 左规,向右移动(即阶码增加)称为 右规。
IEEE 754
计算机是一门工程学科,这意味着很多设计都需要基于实际应用,而 IEEE 754 标准就是理论与实践结合的一个典型示例。
IEEE 754(读作 I trible E 754)标准主要规定了计算机中浮点数的表示方式,几乎所有现代计算机都采用此标准。它与我们上述提到的浮点数表示方式略有不同:
- 将尾数的符号位移到整个浮点数的最高位,而不是在尾数的最高位。显然这样浮点数的符号更直观。
- 规定 32 位浮点数(即
float
)的阶码占 8 位,64 位浮点数(即double
)的阶码占 11 位。那么,32 位浮点数的尾数占 $32-1-8=23$ 位,64 位浮点数的尾数占 $64-1-11=52$ 位。 - 阶码的偏移常数采用 $2^n-1$ 而不是常见的 $2^n$,在 32 位浮点数中阶码长度为 8,因此 32 位浮点数阶码的偏移常数是 $2^8-1=127$。这是因为阶码的全 1 和全 0 被保留作为特殊用途。那么,最小的非保留阶码就是 $[00000001]_2=-126$,最大的阶码就是 $[11111110]_2=127$。被保留的两个阶码在数值上是 $[00000000]_2=-127$ 和 $[11111111]_2=-128$。
- 尾数的左侧有一个隐藏位 1 用于表示数值。这意味着,我们表示的 IEEE 754 浮点数都是 $1.[尾数] \times 2^{[阶码]}$ 形式。这样同时做了规格化,又扩大了浮点数的表示范围。
那么,阶码全 1 和全 0 是什么用途呢?
当阶码全 0 时:
- 尾数为 0:表示 +0 和 -0。
- 尾数不为 0:表示 非规格化数。阶码全为 0 时,说明阶码已经小到了不能够再进行左规的程度,那么尾数的规格化还没有完成,就是非规格化数。这可以用来表示比规格化数更小的数,也就是处理 阶码下溢。
当阶码全 1 时:
- 尾数为 0:表示 $+\infin$ 和 $-\infin$。
- 尾数不为 0:表示无定义数(非数)NaN(not a number)。非数可以表示 $\frac{0}{0}$,$\frac{\infin}{\infin}$,$0 \times \infin$,$\sqrt{-1}$ 等。这些数可以让计算过程出现异常时程序还能继续执行,并且提供错误检测功能。
从数值上来看,阶码全 0 是第二小的数,那么它还可以表示一些小到无法规格化的数;而阶码全 1 只能用来表示一些“无法表示的数”,如正负无穷和非数。以上是个人理解赋予它们的意义,也可以认为规定就是规定,并没有它的意义。
可以发现,IEEE 754 的设计与纯数理知识不同,融入了工程上的考量。