# J.7 浮点算术的其他问题
<!-- source: p1256 -->

结束浮点算术讨论之前，再介绍几个主题。

## 融合乘加

浮点单元最常见的用途可能是矩阵运算，而最频繁的矩阵运算是矩阵与矩阵或向量相乘，归根结底需要计算内积 \(x_1y_1+x_2y_2+\cdots+x_ny_n\)，也就是一连串乘加组合。

为此，IBM RS/6000 引入了用一条指令计算 \(ab+c\) 的**融合乘加**（fused multiply-add）。虽然一条指令需要读取三个操作数，它仍有望提高内积性能。

融合乘加先精确计算 \(ab+c\)，最后只舍入一次。少一次舍入确实能略微提高内积精度，但这不是主要动机。它有两个主要优势：一是舍入可能需要加法，实现代价高，只舍入一次就消去了一次加法。

<!-- source: p1257 -->

二是它增加的准确性，可用于在硬件未直接提供时实现正确舍入的除法和平方根，也可构建高效的多精度浮点软件包。

用融合乘加实现正确舍入除法有很多细节，但基本思想简单。再看 J.6 节的 \(a=13,b=51\)：两位精度下，\(1/b\) 舍入为 \(b'=.020\)，\(ab'\) 舍入为 \(q'=.26\)，并非正确商。应用两次融合乘加即可调整：

$$
r=a-bq',\qquad q''=q'+rb'.
$$

普通两位运算中，\(bq'=51\times.26\) 舍入为 13，因此 \(r=0\)，无法调整。但融合乘加给出 \(r=13-(51\times.26)=-.26\)，再得 \(q''=.26-.0052=.2548\)，正确舍入为 .25。细节见 Montoye、Hokenek 与 Runyon [1990]、Markstein [1990]。

## 精度

本附录讨论的标准规定四种精度：单精度、扩展单精度、双精度、扩展双精度，参数见图 J.7。实现不必全部提供，但鼓励支持“单精度与扩展单精度”，或“单精度、双精度与扩展双精度”两种组合之一。科学计算广泛使用双精度，实际几乎总会实现它。因此，设计者通常只需决定是否提供扩展双精度，以及提供多少位。

Motorola 68882 与 Intel 387 协处理器采用标准允许的最小扩展格式：80 位，其中有效数 64 位。但许多较新的高性能浮点芯片没有实现 80 位扩展精度，原因之一是它与 64 位总线、寄存器配合不便。SPARC V8、PA-RISC 等新体系结构规定 128 位扩展精度，或称四倍精度（quad），并形成 15 位指数、113 位有效数的事实约定。

虽然多数高级语言不允许直接使用扩展精度，它对数学软件作者很有价值。例如，编写计算平面向量 \((x,y)\) 长度 \(\sqrt{x^2+y^2}\) 的库函数，若 \(x>2^{E_{\max}/2}\)，直接计算就会溢出。这意味着要么把函数允许的指数范围减半，要么使用缩放等更复杂的算法。

<!-- source: p1258 -->

有扩展精度时，简单算法就能工作。向量长度本身不复杂，也不难设计避免溢出的算法；但更复杂的问题中，扩展精度可能决定能否采用简单快速的算法。二进制到十进制转换就是好例子。Coonen [1984] 清楚介绍了一种依赖扩展精度的高效算法，Goldberg [1991] 也作了简述。准确计算超越函数同样会因扩展精度而容易许多。

精度还有一个重要问题：**两次舍入**（double rounding）。假设十进制单精度为两位、扩展精度为三位，计算 \(1.9\times.66\)。精确积为 1.254，先舍入到扩展精度得 1.25，再舍入到单精度得 1.2；但直接舍入到单精度应为 1.3。因此，两次舍入未必等于一次。

若硬件只做双精度，能否先算双精度再舍入为单精度来模拟单精度？上述例子似乎说不能，但两次舍入并不总有危险。下列结论成立，证明不容易，见习题 J.25：

> 译注：这里的习题编号沿用原文；本附录实际讨论两次舍入的是习题 J.24，习题 J.25 讨论浮点异常的快速检测。

> 若 x、y 的有效数为 p 位，将 x+y 精确计算后先舍入至 q 位，只要 \(q\ge2p+2\)，再舍入至 p 位就不会改变本应得到的答案。这不只适用于加法，也适用于乘法、除法和平方根。

上面的十进制例子 \(q=3,p=2\)，不满足条件。IEEE 二进制中，双精度 \(q=53\)、单精度 \(p=24\)，有 \(53\ge2\times24+2=50\)。因此，可先正确计算双精度结果，即精确运算后舍入到双精度，再舍入到单精度，以实现单精度运算。

> 译注：原页正文重复这个不等式时，关系符被排为异常字符；此处依据同页完整命题恢复为 \(\ge\)。本段讨论须连同标准的舍入及指数范围条件理解；后文还会专门说明下溢时的两次舍入问题。

## 异常

IEEE 标准定义五类异常：下溢、上溢、除以零、不精确（inexact）和无效操作（invalid）。默认只置标志，计算继续。标志具有 sticky 性质，一旦置位，直到显式清除才复位。标准强烈建议为每类异常提供陷阱使能位。若已使能的异常发生，则调用用户陷阱处理程序，相关异常标志值未定义。J.3 节的 \(\sqrt{-3}=\mathrm{NaN}\)、\(1/0=\infty\) 就是异常：默认前者置无效操作标志并返回 NaN，后者置除以零标志并返回无穷。

<!-- source: p1259 -->

多数其他系统也有下溢、上溢与除以零异常。无效操作针对 \(\sqrt{-1}\)、\(0/0\)、\(\infty-\infty\) 等无法自然赋予普通浮点值或 \(\pm\infty\) 的运算。不精确异常是 IEEE 算术的特色：结果需要舍入或发生上溢时触发。由于除以零和上溢都可能返回无穷，必须查看异常标志才能区分。不精确异常出现得太频繁，几乎谈不上“异常”，因此为它使能陷阱往往严重影响性能。除下溢外，是否使能陷阱不会改变一个运算是否算异常，后面再谈下溢。

标准假定，陷阱发生后可以识别触发它的运算及操作数。流水线或多个算术单元的机器中，仅检查程序计数器可能不够，可能需要硬件支持以准确定位。

还有如下问题：

```text
r1 = r2 / r3
r2 = r4 + r5
```

两条指令可能并行执行。若除法触发陷阱，它的操作数 r2 可能已经被加法覆盖，尤其因为加法通常比除法快得多。支持 IEEE 陷阱的系统必须设法保存原 r2，或者由硬件保存，或者由编译器避免这种情形。这并非浮点独有，例如：

```text
r1 = 0(r2)
r2 = r3
```

等待内存时执行第二条指令很高效；但若访问 `0(r2)` 发生缺页，重启第一条指令所需的 r2 可能已经丢失。

MIPS R3010 的办法，是在指令周期早期识别可能异常的指令。例如，浮点加法只有当至少一个操作数指数为 \(E_{\max}\) 时才可能上溢。这个早期检查是保守的，可能把实际不异常的操作也标出来；但只要这种误报稀少，性能就很好。被标记为可能异常的指令，由陷阱处理程序中的专门代码计算，避免破坏状态。记住，这些问题只在使能陷阱时存在；否则，正常执行中设置异常标志很直接。

## 下溢

前面多次提到，下溢检测比其他异常复杂。IEEE 标准规定，使能下溢陷阱时，若结果是非规格化数，就必须触发陷阱。

<!-- source: p1260 -->

若未使能，则只有损失准确性、结果必须舍入时，才置下溢标志。理由是：若下溢并未损害准确性，就没必要报警；但若用户使能陷阱，可能是想模拟直接置零，因此结果一旦低于 \(1.0\times2^{E_{\min}}\) 就应通知。

所以，无陷阱时，只有结果既非规格化又不精确，才报告下溢。不过，“非规格化”与“不精确”都有不同解释。通常，不精确意味着真实结果不能准确表示、必须舍入。设二进制有效数为三位，采用舍入到最近值，计算

$$
(1.11_2\times2^{-2})(1.11_2\times2^{E_{\min}})
=0.110001_2\times2^{E_{\min}}.
$$

返回 \(0.11_2\times2^{E_{\min}}\)，发生了舍入，应报告不精确。但是否还要报告下溢？渐进下溢损失有效位，是由于指数范围受限。若指数范围无限，结果将是 \(1.10_2\times2^{E_{\min}-1}\)，与渐进下溢的结果完全相同；非规格化有效数位数较少，在本例并未造成额外差别。

标准说明文档 [Cody 等 1984] 鼓励以此为判据：只有实际结果与“相同小数位数、但指数范围足够大”的系统所得结果不同时，才置下溢标志。不过，这种实现较难，因此标准也允许只要结果非规格化且不同于无限精度结果，就置下溢标志。

对于“结果为非规格化”也有两种定义。原书例子是 \(1.10_2\times2^{-1}\) 乘以 \(1.10_2\times2^{E_{\min}}\)，原文把精确积写为 \(0.1111_2\times2^{E_{\min}}\)，舍入后为规格化数 \(1.00_2\times2^{E_{\min}}\)。若此时报告下溢，就是采用**舍入前**规则，因为舍入前非规格化；若不报告，就是采用**舍入后**规则。标准允许任选其一，但所有运算必须保持一致。

> 译注：原页本例的操作数与声称的乘积不一致。所列 \(1.10_2\times1.10_2\times2^{-1}\) 实为 \(1.001_2\)，不是 \(0.1111_2\)。本段保留原例，并仅据其声称的 \(0.1111_2\) 结果解释两种检测时机；不可把原列乘法当作正确演算。

以浮点加法说明这些规则：加减法结果若为非规格化数，它总是精确的，因此不必设置下溢标志。未使能陷阱时，没有异常；使能时，下溢标志值未定义，也无需设置。

最后还有一个细节。未使能陷阱时，\(p\) 位数运算的下溢结果是精度不超过 \(p-1\) 位的非规格化数；使能时，传给处理程序的却是舍入至 \(p\) 位、指数回绕后的结果。

<!-- source: p1261 -->

这就可能出现两次舍入问题。处理程序若想返回非规格化结果，不能只对收到的参数再舍入，否则可能出错。因此，至少还须传递一位额外信息，才有可能正确舍入。
