浮点数表示
实数的二进制表示
实数在计算机中的存储遵循 IEEE 浮点数标准,但在这里为了方便理解 IEEE 标准,这里首先阐明一般 实数 在计算机中是如何存储的。
在这里 实数 分为两个部分存储:整数部分 和 小数部分,两个部分在逻辑上用 · 隔开。
比如对于以下 浮点数 的二进制表示:
其中每一位对应的数值如下表所示:
上述二进制表示对应的数值为
其中 或 ,表示第 i 为是 0 还是 1。
举例说明如何使用上述表示法计算 实数:
- 对应的数值为
- 对应的数值为
字面值转二进制
举个实际的例子,将 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 浮点数表示
上述 浮点数 存储方式的缺点在于如果要表示比较大的数,就需要比较多的二进制位数,比如对于 就需要 103 位。IEEE 表示 就解决了这个问题,在 IEEE 表示 中,浮点数 被表示为 ,其中 的含义如下:
- 为 符号位
- 为 乘法因子
- 为 指数部分
IEEE 754 标准 是定义浮点数表示和算术的国际标准,它定义了多种不同精度的浮点数格式,但最常见的是 单精度 single precesion(32 位)和 双精度 double precesion(64 位),浮点数分为 s(符号位)、exp(阶码)、frac(尾数)三个部分存储:
位的 与上述计算公式中的 符号位 相同,位于浮点数二进制表示的 最高位,标志了浮点数的正负,如果 的话 是负数,如果 的话 为正数
位的 用于计算 指数部分 ( 为 unsigned 值,但不能为全 0 或全 1),其中 ,
- 对于 单精度 浮点数
- ,
- 对于 双精度 浮点数
- ,
- 对于 单精度 浮点数
位的 用于计算 因子 ,其中 , 表示的小数计算方式见 实数的二进制表示
- 对于 单精度 浮点数, , 的最大值为 , 最小值为
- 对于 双精度 浮点数, , 的最大值为 , 最小值为
总结 单精度 和 双精度 浮点数 表示如下:
| 类型 | 符号位 s | 阶码 exp | 尾数 frac | 总位数 | 偏置值 |
|---|---|---|---|---|---|
| 单精度 | 1 | 8 | 23 | 32 | 127 |
| 双精度 | 1 | 11 | 52 | 64 | 1023 |
单精度 浮点数表示为:
双精度 浮点数表示为:
总结 单精度 和 双精度 浮点数中各个字段的范围如下图所示:
异常值
注意 IEEE 浮点数 表示分为 Normalized Values(正常值)和 Denormalized Values(非正常值)以及特殊值,上节中提到的 IEEE 浮点数 计算方法只适用于 正常值,
正常值 的 阶码(exp)不能为全 0 或全 1。
下图是 浮点数 各种类型的图示(以 单精度 为例):
- 非正常值(Denormalized)的 阶码全为 0
- 无穷大(Infinity)的 阶码全为 1,尾数位为 0
- 非数字(NaN,Not a Number)的 阶码全为 1,尾数位不为 0
需要注意的是 非正常值 的 浮点数 计算公式 与 正常值 不同:
注意这里的:
- 没有隐含的 “1”,因为它是 非正规数。
- 指数固定为 (而不是 ,因为 阶码是全 0)
- 这是为了在非常接近 0 的地方保持精度连续性(即从最小正规数向 0 过渡的“填充区”)
下表总结了给出了 浮点数 表示各种情况的有效值计算:
| 类型 | 阶码(exp) | 尾数(fraction) | 有效值公式 |
|---|---|---|---|
| 正常值 | 非全 0、非全 1 | ||
| 非正常值 | 全 0 | (必须非 0,否则是 ) | |
| 全 0 | 全 0 | ||
| 全 1 | 全 0 | ||
| NaN | 全 1 | 非 0 | Not a Number(结果非法或未定义) |
字面值转二进制
前文已经提到如果我们有 浮点数 的 IEEE 二进制表示,如何将其转化为实际的 浮点数,就是通过如下的公式:
还有一种常见的考题是给定我们一个 浮点数字面值,比如 2.25,然后让我们去反推它的二进制表示,这里有没有什么简便的解法呢?
最简便的方法还是反向转换,即将一个 浮点数 表示为 一点几几( )乘以二的多少次方( )的格式,有这两部分可以分别计算出 阶码和尾数,然后符号位由数字的正负判断。
以 2.25 为例,如果我们想得到该 浮点数 的 单精度 表示:
由此可以计算出 浮点数 的各个部分:
- 符号位 为 0
- 阶码 为
,二进制表示为
1000 0000B - 尾数 为
0010 0000 0000 ....
所以 浮点数 的二进制为 0100 0000 0001 0000 ...,十六进制为 40100000H。
一般而言,试题只会考察这种没有精度损失的 浮点数二进制转换,对于有精度损失的情况,由于比较繁琐,基本不会考察,其 尾数 部分的计算与 一般实数字面值转二进制 相同。
表示精度
上述的例子是一个比较理想的例子,2.25 可以精确地用 浮点数 表示。但在真实场景中,很多 实数 都无法精确地用 浮点数 表示。比如 1.2 这个数:
其中
是一个无限循环二进制小数,因此无法用有限位的 尾数 精确表示,只能选择一个最接近的值进行近似,这也是 浮点数精度误差 的来源。
若要理解这种近似过程,我们首先需要理解 精度 的概念。
为了便于说明,暂时忽略 规格化,假设仅使用 3 位二进制小数 来表示区间 [0, 1] 中的小数,则能够精确表示的数为:
即:
在数轴中对应 [0, 1] 区间上的 8 个离散点:
如果某个小数恰好不在这些离散点上,例如 0.3,则只能选择其中距离最近的一个点来近似表示,这就是 浮点数表示误差 的来源。
随着 尾数 位数不断增加,相邻两个可表示数之间的间隔会越来越小,因此能够更精确地逼近真实数值。
例如:
- 单精度浮点数 的 尾数(frac)为 23 位,在固定指数下,相邻两个可表示数之间的间隔约为
- 双精度浮点数 的 尾数 为 52 位,在固定指数下,相邻两个可表示数之间的间隔约为
因此,尾数位数越多,相邻两个可表示浮点数之间的距离越小,也就意味着 精度越高,用来近似表示无法精确表示的实数时,误差也越小。
舍入(Rounding) 是将一个实数转换为指定浮点精度的过程。由于许多数学运算的结果无法用有限位的 浮点数 精确表示,因此需要将其舍入到距离最近的可表示浮点数。IEEE 754 默认采用**舍入到最近值(Round to Nearest, Ties to Even)**作为默认舍入方式。
浮点数加减
浮点数 加减计算通常包含以下几个步骤:
- 对阶
- 尾数加减
- 规格化(右规或左规)
- 舍入
对阶
为了进行加减运算,两个 浮点数 必须具有相同的阶码,因此采用小阶向大阶对齐的原则:
- 比较两个浮点数的 阶码。
- 将阶码较小的浮点数的 尾数右移,直到两个阶码相等。
- 阶码较大的浮点数保持不变。
例如:
将第二个数向第一个数对阶:
因此,对阶的本质是:
阶码较小的数,尾数右移,阶码增大;最终两个阶码相等。
这里有一个容易考察的结论:
对阶不会引起阶码上溢或下溢。
因为对阶只是把较小的阶码增加到较大的阶码,而较大的阶码本身已经是两个操作数中较大的那个阶码。只要输入浮点数本身合法,对阶过程中不会因为阶码调整而发生溢出。
但是,对阶过程中可能发生精度损失:
例如有限位尾数在右移时,最右侧的 1 可能被移出去,从而产生舍入误差。
尾数加减
完成对阶后,两个浮点数的阶码相同,可以直接进行尾数加减。
例如:
则:
得到:
此时尾数已经不符合规格化形式,需要进行右规。
尾数规格化
尾数运算后,需要将结果调整为规定的规格化形式。
对于二进制规格化浮点数,通常要求尾数为:
因此,根据尾数运算结果的不同,可能进行右规或左规。
右规如果尾数出现进位,例如:
需要将尾数右移一位:
即:
右规:尾数右移,阶码加 1。
因此右规可能导致阶码上溢。
左规如果尾数过小,例如出现:
则需要不断左移尾数,直到恢复为 1.xxxx:
即:
左规:尾数左移,阶码减 1。
因此左规可能导致阶码下溢。
所以可以直接记:
舍入
由于浮点数的尾数位数有限,在对阶、尾数运算或规格化后,如果有效位超过了规定的尾数长度,就需要进行舍入。
常见的舍入方式包括:
- 向零舍入
- 向上舍入
- 向下舍入
- 就近舍入(中间值取偶数)
需要特别注意:
舍入本身也可能产生进位,从而导致阶码增加 1。
例如,假设当前规格化尾数已经接近:
舍入后可能产生:
此时需要再次进行右规:
因此:
这也是题目中“右规和尾数舍入都可能引起阶码上溢”这一说法成立的原因。
溢出判断
这是浮点数加减中非常容易混淆的一点。
尾数运算产生溢出,并不意味着最终浮点数结果一定溢出。
例如两个正数相加:
尾数相加:
虽然尾数产生了进位,但经过右规:
只要新的阶码仍然处于可表示范围内,最终结果就是正常可表示的浮点数。
因此:
真正决定浮点数是否发生阶码溢出的,是规格化、舍入等操作之后,最终阶码是否超出了可表示范围。
总结浮点数加减操作如下:
| 操作 | 尾数变化 | 阶码变化 | 可能导致 |
|---|---|---|---|
| 对阶 | 小阶尾数右移 | 小阶码增大 | 精度损失,不导致阶码溢出 |
| 尾数加减 | 尾数进行加减 | 不变 | 可能产生尾数进位/借位 |
| 右规 | 尾数右移 | 阶码 +1 | 可能阶码上溢 |
| 左规 | 尾数左移 | 阶码 −1 | 可能阶码下溢 |
| 舍入 | 尾数可能加 1 | 可能 +1 | 可能阶码上溢 |