浮点数的加减运算
定点加法只有一个步骤:把两个数送进加法器。浮点加法有五个步骤,多出来的四步全是为了处理”两个数的小数点不在同一位置”这件事。
机制
第一步:对阶——小阶向大阶看齐
两个浮点数的阶码不同,意味着它们的尾数权值不同,直接相加毫无意义。必须先让阶码相等。
规则:求阶差
疑问点:对阶为何一律向大阶看齐,而不是反过来
两种做法都能使阶码相等,但结果的可靠性完全不同。
理由是”丢哪一头”:
- 小阶向大阶看齐:小数的尾数右移,移出去的是最低位。丢的是精度,误差有界且很小。
- 大阶向小阶看齐:大数的尾数左移,移出去的是最高有效位。丢的是量级,结果会彻底错误。
这不是两害相权取其轻的问题,后者是不可接受的。 右移丢低位得到的是”近似正确”,左移丢高位得到的是”完全错误”——所以只有一种选择。
另外注意:右移的是尾数,且必须是算术右移(尾数为补码时补符号位),否则符号会被破坏。
尾数对齐时的信息丢失
对阶天然会丢信息,当阶差大于尾数位数时,小数会被完全移空:
float a = 1e8f;
float b = a + 1.0f; /* b == a,加了个 1 等于没加 */float 只有 24 位有效位。要把
这就是”大数吃小数”。它的一般判据是:当两数相差超过 float 的 double 的
由此得到一个实用结论:求和一大批浮点数时,从小到大加比从大到小加精度高得多——先把小数攒成中等大小的数,再去和大数相加,就不会被整个吃掉。
第二步:尾数求和
对阶之后两个尾数已经同权,按定点补码加减法直接相加即可,没有任何新东西。
结果可能有三种形态,正好对应下一步的三条分支。
第三步:规格化——左规与右规
右规:尾数溢出时用。
两个同号规格化数相加,尾数可能变成
右规最多只需要一次。 因为两个规格化数的尾数绝对值都小于 2,和必小于 4,右移一位一定落回
左规:尾数不规格化时用。
两个异号数相加(即减法)时,高位可能大量抵消,尾数变成
左规可能需要多次,最多移到尾数位数用尽(此时结果是 0)。
这里藏着浮点运算最危险的现象——“抵消”(catastrophic cancellation):左规把低位的舍入误差也一起左移到了高位,原本无关紧要的末位误差被放大成了结果的高位。两个相近的数相减,结果的有效位数会急剧减少。 数值计算中要尽量避免这种减法。
| 触发条件 | 移动方向 | 阶码 | 次数 | |
|---|---|---|---|---|
| 右规 | 尾数溢出( | 尾数右移 | 至多 1 次 | |
| 左规 | 尾数最高位为 0 | 尾数左移 | 可能多次 |
第四步:舍入
对阶时右移出去的位、右规时移出去的位,不能简单丢弃,否则误差会系统性地偏向一侧。
IEEE 754 规定四种舍入模式,默认是第一种:
| 模式 | 规则 |
|---|---|
| 就近舍入到偶(默认) | 取最接近的可表示数;正好在中间时取尾数末位为 0(偶)的那个 |
| 朝零舍入 | 直接截断 |
| 朝 | 向上取 |
| 朝 | 向下取 |
“正好在中间时取偶”这条规则的理由是统计性的:如果一律”逢中进位”,大量运算下误差会单向累积;取偶则使进位与舍去各占一半,误差期望为 0。
要正确舍入,运算过程中必须多保留几位,IEEE 754 规定三个附加位:
| 位 | 作用 |
|---|---|
| 保护位 G | 紧邻尾数末位的下一位 |
| 舍入位 R | 再下一位 |
| 粘滞位 S | 再往下所有位的”或”——只要还有任何一个 1,S 就是 1 |
粘滞位的存在就是为了区分”正好在中间”和”略超过中间”:
第五步:判溢出——只看阶码
这一步的判据只有一条,也是本节最高频的考点:
- 阶码上溢(超出最大阶)→ 浮点上溢,结果置
或 ,是真正的错误。 - 阶码下溢(低于最小阶)→ 浮点下溢,结果进入非规格化数,再小则置
。下溢通常不视为错误,只是精度损失。
尾数溢出不是浮点溢出——它只是触发一次右规,右规后阶码若仍在范围内,结果完全正确。
判溢出必须放在最后,因为规格化会改变阶码:左规使阶码变小(可能下溢),右规使阶码变大(可能上溢),在规格化之前判断毫无意义。
完整流程
flowchart TD A["① <b>对阶</b><br/>小阶向大阶看齐<br/>尾数算术右移"]:::step B["② <b>尾数求和</b><br/>定点补码加减"]:::step C{"尾数形态?"}:::chk D["③a <b>右规</b><br/>尾数右移 1 位<br/>阶码 +1"]:::step E["③b <b>左规</b><br/>尾数左移 k 位<br/>阶码 −k"]:::step F["④ <b>舍入</b><br/>就近舍入到偶<br/>用 G/R/S 三位判断"]:::step G{"⑤ <b>阶码</b>越界?"}:::chk H["上溢 → ±∞"]:::err I["下溢 → 非规格化数 / ±0"]:::warn J["结果正确"]:::ok A --> B --> C C -->|"溢出 ≥2"| D C -->|"最高位为 0"| E C -->|"已规格化"| F D --> F E --> F F --> G G -->|"阶码上溢"| H G -->|"阶码下溢"| I G -->|"否"| J classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef chk fill:#f1f5f9,stroke:#94a3b8,color:#334155 classDef err fill:#fee2e2,stroke:#dc2626,color:#7f1d1d classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
一道完整的手算
计算
① 对阶:
② 尾数求和:
③ 规格化:尾数
④ 舍入:尾数字段只留 4 位小数 →
⑤ 判溢出:阶码 4 在范围内,结果为
验算:
这个例子演示了一个通用现象:右规使阶码从 3 变成 4,同一批尾数位所代表的绝对精度随之翻倍(间隔从
浮点乘除的主线
乘除法比加减法简单,因为不需要对阶:
- 乘法:阶码相加,尾数相乘,然后规格化、舍入、判溢出。
- 除法:阶码相减,尾数相除,同样收尾。
阶码相加时要注意偏置被加了两次,必须减去一个偏置:
边界
尾数溢出 vs 浮点溢出
| 尾数溢出 | 浮点溢出 | |
|---|---|---|
| 现象 | 尾数绝对值 | 阶码超出表示范围 |
| 是不是错误 | 不是,右规即可修正 | 上溢是;下溢通常不是 |
| 判断时机 | 尾数求和后 | 规格化之后 |
“浮点运算的溢出只看阶码”是本节第一考点。
浮点加法不满足结合律
具体例子(float):
差别就在第二个式子里
同理,浮点加法也不满足分配律,且
但交换律是成立的:
左规为什么危险
右规损失的是最低位,误差有界。左规则相反:它把低位(很可能是上一次运算残留的舍入误差)左移到高位。
考试层面记住”左规可能多次、右规至多一次”即可;工程层面这是浮点算法设计的核心问题。
舍入发生在哪几个位置
不止一处,这是容易漏的:
- 对阶时右移出的位;
- 右规时右移出的位;
- 十进制常量转二进制时(2.1.1),在编译期就已经舍入过一次。
所以 0.1f + 0.2f != 0.3f 的误差在源代码被编译的那一刻就已经产生了,与后续运算无关。
对照速查
| 步骤 | 关键点 |
|---|---|
| ① 对阶 | 小阶向大阶;尾数算术右移 |
| ② 求和 | 定点补码加减 |
| ③ 规格化 | 右规至多 1 次;左规可能多次 |
| ④ 舍入 | 默认就近舍入到偶;需 G/R/S 三位 |
| ⑤ 判溢出 | 只看阶码;必须放在最后 |
| 陷阱 | 结论 |
|---|---|
| 尾数溢出算浮点溢出吗 | 不算 |
| 浮点加法有结合律吗 | 没有;交换律有 |
| 阶差 > 有效位数 | 小数被完全吃掉 |
| 浮点乘法的阶码 |
考点
- 对阶一律小阶向大阶看齐,理由是右移丢低位、左移丢高位
- 右规至多一次,左规可能多次
- 溢出只看阶码;尾数溢出靠右规修正,不算溢出
- 判溢出必须在规格化之后
- 默认舍入是就近舍入到偶,理由是使误差期望为 0
- 粘滞位用来区分”正好一半”和”超过一半”
- 大数吃小数:阶差超过有效位数时小数完全消失
- 浮点加法无结合律,
与 不等 - 浮点乘法阶码相加后要减去一个偏置
链接
- 🏠 返回总览:计算机组成原理第 2 章:数据的表示和运算总览
- ⬅️ 上一节:2.3.1 浮点数的表示
- ➡️ 下一节:2.3.3 C 语言中的浮点数类型
- 📖 名词库:第 2 章名词库