J.7 浮点算术的其他问题
结束浮点算术讨论之前,再介绍几个主题。
融合乘加
浮点单元最常见的用途可能是矩阵运算,而最频繁的矩阵运算是矩阵与矩阵或向量相乘,归根结底需要计算内积 ,也就是一连串乘加组合。
为此,IBM RS/6000 引入了用一条指令计算 的融合乘加(fused multiply-add)。虽然一条指令需要读取三个操作数,它仍有望提高内积性能。
融合乘加先精确计算 ,最后只舍入一次。少一次舍入确实能略微提高内积精度,但这不是主要动机。它有两个主要优势:一是舍入可能需要加法,实现代价高,只舍入一次就消去了一次加法。
二是它增加的准确性,可用于在硬件未直接提供时实现正确舍入的除法和平方根,也可构建高效的多精度浮点软件包。
用融合乘加实现正确舍入除法有很多细节,但基本思想简单。再看 J.6 节的 :两位精度下, 舍入为 , 舍入为 ,并非正确商。应用两次融合乘加即可调整:
普通两位运算中, 舍入为 13,因此 ,无法调整。但融合乘加给出 ,再得 ,正确舍入为 .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 位有效数的事实约定。
虽然多数高级语言不允许直接使用扩展精度,它对数学软件作者很有价值。例如,编写计算平面向量 长度 的库函数,若 ,直接计算就会溢出。这意味着要么把函数允许的指数范围减半,要么使用缩放等更复杂的算法。
有扩展精度时,简单算法就能工作。向量长度本身不复杂,也不难设计避免溢出的算法;但更复杂的问题中,扩展精度可能决定能否采用简单快速的算法。二进制到十进制转换就是好例子。Coonen [1984] 清楚介绍了一种依赖扩展精度的高效算法,Goldberg [1991] 也作了简述。准确计算超越函数同样会因扩展精度而容易许多。
精度还有一个重要问题:两次舍入(double rounding)。假设十进制单精度为两位、扩展精度为三位,计算 。精确积为 1.254,先舍入到扩展精度得 1.25,再舍入到单精度得 1.2;但直接舍入到单精度应为 1.3。因此,两次舍入未必等于一次。
若硬件只做双精度,能否先算双精度再舍入为单精度来模拟单精度?上述例子似乎说不能,但两次舍入并不总有危险。下列结论成立,证明不容易,见习题 J.25:
译注:这里的习题编号沿用原文;本附录实际讨论两次舍入的是习题 J.24,习题 J.25 讨论浮点异常的快速检测。
若 x、y 的有效数为 p 位,将 x+y 精确计算后先舍入至 q 位,只要 ,再舍入至 p 位就不会改变本应得到的答案。这不只适用于加法,也适用于乘法、除法和平方根。
上面的十进制例子 ,不满足条件。IEEE 二进制中,双精度 、单精度 ,有 。因此,可先正确计算双精度结果,即精确运算后舍入到双精度,再舍入到单精度,以实现单精度运算。
译注:原页正文重复这个不等式时,关系符被排为异常字符;此处依据同页完整命题恢复为 。本段讨论须连同标准的舍入及指数范围条件理解;后文还会专门说明下溢时的两次舍入问题。
异常
IEEE 标准定义五类异常:下溢、上溢、除以零、不精确(inexact)和无效操作(invalid)。默认只置标志,计算继续。标志具有 sticky 性质,一旦置位,直到显式清除才复位。标准强烈建议为每类异常提供陷阱使能位。若已使能的异常发生,则调用用户陷阱处理程序,相关异常标志值未定义。J.3 节的 、 就是异常:默认前者置无效操作标志并返回 NaN,后者置除以零标志并返回无穷。
多数其他系统也有下溢、上溢与除以零异常。无效操作针对 、、 等无法自然赋予普通浮点值或 的运算。不精确异常是 IEEE 算术的特色:结果需要舍入或发生上溢时触发。由于除以零和上溢都可能返回无穷,必须查看异常标志才能区分。不精确异常出现得太频繁,几乎谈不上“异常”,因此为它使能陷阱往往严重影响性能。除下溢外,是否使能陷阱不会改变一个运算是否算异常,后面再谈下溢。
标准假定,陷阱发生后可以识别触发它的运算及操作数。流水线或多个算术单元的机器中,仅检查程序计数器可能不够,可能需要硬件支持以准确定位。
还有如下问题:
两条指令可能并行执行。若除法触发陷阱,它的操作数 r2 可能已经被加法覆盖,尤其因为加法通常比除法快得多。支持 IEEE 陷阱的系统必须设法保存原 r2,或者由硬件保存,或者由编译器避免这种情形。这并非浮点独有,例如:
等待内存时执行第二条指令很高效;但若访问 0(r2) 发生缺页,重启第一条指令所需的 r2 可能已经丢失。
MIPS R3010 的办法,是在指令周期早期识别可能异常的指令。例如,浮点加法只有当至少一个操作数指数为 时才可能上溢。这个早期检查是保守的,可能把实际不异常的操作也标出来;但只要这种误报稀少,性能就很好。被标记为可能异常的指令,由陷阱处理程序中的专门代码计算,避免破坏状态。记住,这些问题只在使能陷阱时存在;否则,正常执行中设置异常标志很直接。
下溢
前面多次提到,下溢检测比其他异常复杂。IEEE 标准规定,使能下溢陷阱时,若结果是非规格化数,就必须触发陷阱。
若未使能,则只有损失准确性、结果必须舍入时,才置下溢标志。理由是:若下溢并未损害准确性,就没必要报警;但若用户使能陷阱,可能是想模拟直接置零,因此结果一旦低于 就应通知。
所以,无陷阱时,只有结果既非规格化又不精确,才报告下溢。不过,“非规格化”与“不精确”都有不同解释。通常,不精确意味着真实结果不能准确表示、必须舍入。设二进制有效数为三位,采用舍入到最近值,计算
返回 ,发生了舍入,应报告不精确。但是否还要报告下溢?渐进下溢损失有效位,是由于指数范围受限。若指数范围无限,结果将是 ,与渐进下溢的结果完全相同;非规格化有效数位数较少,在本例并未造成额外差别。
标准说明文档 [Cody 等 1984] 鼓励以此为判据:只有实际结果与“相同小数位数、但指数范围足够大”的系统所得结果不同时,才置下溢标志。不过,这种实现较难,因此标准也允许只要结果非规格化且不同于无限精度结果,就置下溢标志。
对于“结果为非规格化”也有两种定义。原书例子是 乘以 ,原文把精确积写为 ,舍入后为规格化数 。若此时报告下溢,就是采用舍入前规则,因为舍入前非规格化;若不报告,就是采用舍入后规则。标准允许任选其一,但所有运算必须保持一致。
译注:原页本例的操作数与声称的乘积不一致。所列 实为 ,不是 。本段保留原例,并仅据其声称的 结果解释两种检测时机;不可把原列乘法当作正确演算。
以浮点加法说明这些规则:加减法结果若为非规格化数,它总是精确的,因此不必设置下溢标志。未使能陷阱时,没有异常;使能时,下溢标志值未定义,也无需设置。
最后还有一个细节。未使能陷阱时, 位数运算的下溢结果是精度不超过 位的非规格化数;使能时,传给处理程序的却是舍入至 位、指数回绕后的结果。
这就可能出现两次舍入问题。处理程序若想返回非规格化结果,不能只对收到的参数再舍入,否则可能出错。因此,至少还须传递一位额外信息,才有可能正确舍入。