浮点数的加减运算

定点加法只有一个步骤:把两个数送进加法器。浮点加法有五个步骤,多出来的四步全是为了处理”两个数的小数点不在同一位置”这件事。对阶尾数求和规格化舍入判溢出这五步的顺序不能调换,每一步都为下一步创造前提;理解每一步”为什么必须在这个位置”,比背步骤名有用得多。

机制

第一步:对阶——小阶向大阶看齐

两个浮点数的阶码不同,意味着它们的尾数权值不同,直接相加毫无意义。必须先让阶码相等。

规则:求阶差 ,把阶码小的那个数的尾数右移 位,同时把它的阶码加 ,直到两阶相等。

疑问点:对阶为何一律向大阶看齐,而不是反过来

两种做法都能使阶码相等,但结果的可靠性完全不同。

理由是”丢哪一头”:

  • 小阶向大阶看齐:小数的尾数右移,移出去的是最低位。丢的是精度,误差有界且很小。
  • 大阶向小阶看齐:大数的尾数左移,移出去的是最高有效位。丢的是量级,结果会彻底错误。

这不是两害相权取其轻的问题,后者是不可接受的。 右移丢低位得到的是”近似正确”,左移丢高位得到的是”完全错误”——所以只有一种选择。

另外注意:右移的是尾数,且必须是算术右移(尾数为补码时补符号位),否则符号会被破坏。

尾数对齐时的信息丢失

对阶天然会丢信息,当阶差大于尾数位数时,小数会被完全移空:

float a = 1e8f;
float b = a + 1.0f;    /* b == a,加了个 1 等于没加 */

的阶约为 ,而 float 只有 24 位有效位。要把 对齐到 这一档,需要右移 26 位,尾数全部移出,剩下 0。于是加法变成”加 0”。

这就是”大数吃小数”。它的一般判据是:当两数相差超过 倍( 为有效位数)时,小的那个在加法中完全消失。 float 的 ,double 的 。

由此得到一个实用结论:求和一大批浮点数时,从小到大加比从大到小加精度高得多——先把小数攒成中等大小的数,再去和大数相加,就不会被整个吃掉。

第二步:尾数求和

对阶之后两个尾数已经同权,按定点补码加减法直接相加即可,没有任何新东西。

结果可能有三种形态,正好对应下一步的三条分支。

第三步:规格化——左规与右规

右规:尾数溢出时用。

两个同号规格化数相加,尾数可能变成 的形式(绝对值 )。此时尾数右移一位,阶码加 1。

右规

右规最多只需要一次。 因为两个规格化数的尾数绝对值都小于 2,和必小于 4,右移一位一定落回 。

左规:尾数不规格化时用。

两个异号数相加(即减法)时,高位可能大量抵消,尾数变成 。此时尾数左移,阶码减,直到最高位为 1。

左规位

左规可能需要多次,最多移到尾数位数用尽(此时结果是 0)。

这里藏着浮点运算最危险的现象——“抵消”(catastrophic cancellation):左规把低位的舍入误差也一起左移到了高位,原本无关紧要的末位误差被放大成了结果的高位。两个相近的数相减,结果的有效位数会急剧减少。 数值计算中要尽量避免这种减法。

触发条件移动方向阶码次数
右规尾数溢出()尾数右移至多 1 次
左规尾数最高位为 0尾数左移移动位数可能多次

第四步:舍入

对阶时右移出去的位、右规时移出去的位,不能简单丢弃,否则误差会系统性地偏向一侧。

IEEE 754 规定四种舍入模式,默认是第一种:

模式规则
就近舍入到偶(默认)取最接近的可表示数;正好在中间时取尾数末位为 0(偶)的那个
朝零舍入直接截断
朝 舍入向上取
朝 舍入向下取

“正好在中间时取偶”这条规则的理由是统计性的:如果一律”逢中进位”,大量运算下误差会单向累积;取偶则使进位与舍去各占一半,误差期望为 0。

要正确舍入,运算过程中必须多保留几位,IEEE 754 规定三个附加位:

位作用
保护位 G紧邻尾数末位的下一位
舍入位 R再下一位
粘滞位 S再往下所有位的”或”——只要还有任何一个 1,S 就是 1

粘滞位的存在就是为了区分”正好在中间”和”略超过中间”: 是正好一半(触发取偶规则); 且 或 为 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 位小数)。

① 对阶:, 的尾数右移 2 位:。

② 尾数求和:,阶码仍为 3。

③ 规格化:尾数 ,右规一次,尾数右移、阶码 :。

④ 舍入:尾数字段只留 4 位小数 → 。保护位 ,不足一半,直接舍去:。

⑤ 判溢出:阶码 4 在范围内,结果为 。

验算:,,真值 。4 位尾数在 这一档的可表示值间隔是 ,最接近 的可表示值就是 ,误差 全部来自第 ④ 步的舍入,与前四步的运算无关。

这个例子演示了一个通用现象:右规使阶码从 3 变成 4,同一批尾数位所代表的绝对精度随之翻倍(间隔从 变成 )。所以右规不只是”移一位”,它同时把结果的绝对误差上限放大了一倍。

浮点乘除的主线

乘除法比加减法简单,因为不需要对阶:

  • 乘法:阶码相加,尾数相乘,然后规格化、舍入、判溢出。
  • 除法:阶码相减,尾数相除,同样收尾。

阶码相加时要注意偏置被加了两次,必须减去一个偏置:。这是浮点乘法题的固定陷阱。

边界

尾数溢出 vs 浮点溢出

尾数溢出浮点溢出
现象尾数绝对值 阶码超出表示范围
是不是错误不是,右规即可修正上溢是;下溢通常不是
判断时机尾数求和后规格化之后

“浮点运算的溢出只看阶码”是本节第一考点。

浮点加法不满足结合律

(一般情况下)

具体例子(float):,,。

差别就在第二个式子里 触发了大数吃小数。

同理,浮点加法也不满足分配律,且 。 定点整数运算(在不溢出时)满足这些定律,浮点不满足——这是两者最重要的行为差异,也是 2.3.3 里各种”C 浮点陷阱”的统一解释。

但交换律是成立的:,因为对阶和求和都与顺序无关。

左规为什么危险

右规损失的是最低位,误差有界。左规则相反:它把低位(很可能是上一次运算残留的舍入误差)左移到高位。

若两数各自都带有末位误差,相减后有效位只剩 1 位,但左规会把它撑成一个”看起来有 24 位有效数字”的结果——精度信息在这一步被彻底伪造。

考试层面记住”左规可能多次、右规至多一次”即可;工程层面这是浮点算法设计的核心问题。

舍入发生在哪几个位置

不止一处,这是容易漏的:

  1. 对阶时右移出的位;
  2. 右规时右移出的位;
  3. 十进制常量转二进制时(2.1.1),在编译期就已经舍入过一次。

所以 0.1f + 0.2f != 0.3f 的误差在源代码被编译的那一刻就已经产生了,与后续运算无关。

对照速查

步骤关键点
① 对阶小阶向大阶;尾数算术右移
② 求和定点补码加减
③ 规格化右规至多 1 次;左规可能多次
④ 舍入默认就近舍入到偶;需 G/R/S 三位
⑤ 判溢出只看阶码;必须放在最后
陷阱结论
尾数溢出算浮点溢出吗不算
浮点加法有结合律吗没有;交换律有
阶差 > 有效位数小数被完全吃掉
浮点乘法的阶码偏置

考点

  • 对阶一律小阶向大阶看齐,理由是右移丢低位、左移丢高位
  • 右规至多一次,左规可能多次
  • 溢出只看阶码;尾数溢出靠右规修正,不算溢出
  • 判溢出必须在规格化之后
  • 默认舍入是就近舍入到偶,理由是使误差期望为 0
  • 粘滞位用来区分”正好一半”和”超过一半”
  • 大数吃小数:阶差超过有效位数时小数完全消失
  • 浮点加法无结合律, 与 不等
  • 浮点乘法阶码相加后要减去一个偏置

链接