跳转到主要内容

J.6 除法与余数

本节讨论浮点除法与余数。

迭代除法

前面介绍过整数除法。将它改成浮点算法,与把整数乘法改成浮点乘法类似。由

s1×2e1s2×2e2=s1s2×2e1−e2 \frac{s_1\times2^{e_1}}{s_2\times2^{e_2}}=\frac{s_1}{s_2}\times2^{e_1-e_2}

可知,只需用除法器计算 s1/s2s_1/s_2,再乘以 2e1−e22^{e_1-e_2}。图 J.2(b) 中的操作数装入位置与整数除法略有不同:s2s_2 装入 B,s1s_1 装入 P,A 无需保存操作数。随后使用整数除法算法,仅省去最初的一次左移,结果形式为 q0.q1⋯q_0.q_1\cdots。为舍入,再多算两个商位,分别作为保护位和舍入位,并用余数是否非零作为 sticky 位。保护位是必要的,因为第一个商位可能为 0;但分子分母均已规格化,商的最高两位不可能都为 0。这个算法每步产生一个商位。

另一种除法方法以二次而非线性速率收敛到商,J.10 节会介绍采用它的实际机器。下面先介绍两种主要迭代算法,再与直接除法算法比较。构造迭代算法的一般方法是 Newton 迭代,见图 J.13。先把问题变成求函数零点,再从某个零点估计出发,以该点的切线近似函数,用切线零点作为新估计。若 xix_i 是估计值,切线方程为

y−f(xi)=f′(xi)(x−xi), y-f(x_i)=f'(x_i)(x-x_i),

其零点为

x=xi+1=xi−f(xi)f′(xi).(J.6.1) x=x_{i+1}=x_i-\frac{f(x_i)}{f'(x_i)}.\tag{J.6.1}
用切线与横轴交点改进零点估计的 Newton 迭代
图 J.13 用 Newton 迭代求零点。若 xi 是 f 的零点估计,xi+1 就是改进后的估计。求 f 在 xi 处的切线与 x 轴的交点,即得到 xi+1。

取 f(x)=x−1−bf(x)=x^{-1}-b,其零点为 1/b1/b,所以 Newton 迭代给出从 bb 求倒数的方法。因 f′(x)=−1/x2f'(x)=-1/x^2,式 J.6.1 化为

xi+1=xi−1/xi−b−1/xi2=xi+xi−xi2b=xi(2−xib).(J.6.2) x_{i+1}=x_i-\frac{1/x_i-b}{-1/x_i^2} =x_i+x_i-x_i^2b=x_i(2-x_ib).\tag{J.6.2}

因此,可如下计算 a/ba/b:

  1. 缩放 bb,使 1≤b<21\le b<2,查表取得 1/b1/b 的近似值 x0x_0。
  2. 迭代 xi+1=xi(2−xib)x_{i+1}=x_i(2-x_ib),直到 xnx_n 足够准确。
  3. 计算 axnax_n,并撤销第 1 步的缩放。

第 2 步需要多少次?精确到 pp 位意味着相对误差的绝对值为 2−p2^{-p}。简单代数计算表明,下一步的相对误差绝对值为 2−2p2^{-2p},所以正确位数每步翻倍。Newton 迭代具有自校正性质:xix_i 出现误差并不致命,它只是被当作 1/b1/b 的一个估计,下一步会改进它,正确位数大致翻倍。舍入会造成误差;更重要的是,初始几次迭代本来就不期待很多正确位,因此可降低乘法精度,加快运算而不损害最终准确性。习题 J.20 讨论另一种应用。

译注:原页把下一步相对误差写为正的 2−2p2^{-2p},但没有绝对值。令 ϵi=bxi−1\epsilon_i=bx_i-1,严格关系是 ϵi+1=−ϵi2\epsilon_{i+1}=-\epsilon_i^2;正确位数翻倍的结论针对误差绝对值。

第二种方法常称为 Goldschmidt 算法。计算 a/ba/b 时,让分子分母同时乘以使 rb≈1rb\approx1 的数 rr。具体令 x0=a,y0=bx_0=a,y_0=b,每步计算 xi+1=rixix_{i+1}=r_ix_i、yi+1=riyiy_{i+1}=r_iy_i,则 xi+1/yi+1=xi/yi=a/bx_{i+1}/y_{i+1}=x_i/y_i=a/b 始终不变。选择 rir_i 使 yi→1y_i\to1,便有 xi→a/bx_i\to a/b。

同样思想也能计算其他函数。例如,求 a\sqrt a,令 x0=y0=ax_0=y_0=a,每步计算 xi+1=ri2xix_{i+1}=r_i^2x_i、yi+1=riyiy_{i+1}=r_iy_i,则 xi+1/yi+12=xi/yi2=1/ax_{i+1}/y_{i+1}^2=x_i/y_i^2=1/a。若选择 rir_i 使 xi→1x_i\to1,就有 yi→ay_i\to\sqrt a。TI 8847 用这种技术求平方根。

回到除法。令 x0=a,y0=bx_0=a,y_0=b,写 b=1−δb=1-\delta,其中 ∣δ∣<1|\delta|<1。若取 r0=1+δr_0=1+\delta,则 y1=1−δ2y_1=1-\delta^2;再取 r1=1+δ2r_1=1+\delta^2,则 y2=1−δ4y_2=1-\delta^4,依此类推。因为 ∣δ∣<1|\delta|<1,所以 yi→1y_i\to1。相应有

xi+1=rixi=(1+δ2i)xi=[1+(1−b)2i]xi, x_{i+1}=r_ix_i=(1+\delta^{2^i})x_i =\left[1+(1-b)^{2^i}\right]x_i,

即

xi+1=a[1+(1−b)][1+(1−b)2][1+(1−b)4]⋯[1+(1−b)2i].(J.6.3) x_{i+1}=a[1+(1-b)][1+(1-b)^2][1+(1-b)^4]\cdots[1+(1-b)^{2^i}].\tag{J.6.3}

这个算法似乎有两个问题:bb 离 1 较远,即 δ\delta 离 0 较远时,收敛慢;另外它不自校正,因为商是若干独立因子的乘积,其中一个因子的误差不会自行纠正。解决前者,可先查得 bb 的近似倒数 b′b',再对 ab′/bb′ab'/bb' 运行算法;因为 bb′≈1bb'\approx1,收敛很快。

为应对后者,运算需多保留几位,以补偿舍入误差。不过,Goldschmidt 算法仍有较弱的自校正性质:rir_i 的精确值并不重要。前几次迭代无需 1−δ2i1-\delta^{2^i} 的完整精度,可以把 rir_i 取为 1+δ2i1+\delta^{2^i} 的截断值,以加快迭代而不影响收敛速度。此时 yiy_i 不再恰为 1−δ2i1-\delta^{2^i},式 J.6.3 不能直接使用,但很容易组织成不依赖 rir_i 精确值的算法:

  1. 缩放 a,ba,b,使 1≤b<21\le b<2。
  2. 查表取得 1/b1/b 的近似值 b′b'。
  3. 令 x0=ab′x_0=ab'、y0=bb′y_0=bb'。
  4. 重复下列循环,直到 xix_i 足够接近 a/ba/b:
r≈2−y[yi=1+δi ⇒ r≈1−δi],y:=y×r[yi+1=yir≈1−δi2],xi+1:=xi×r[xi+1=xir]. \begin{aligned} r&\approx2-y &&[y_i=1+\delta_i\ \Rightarrow\ r\approx1-\delta_i],\\ y&:=y\times r &&[y_{i+1}=y_i r\approx1-\delta_i^2],\\ x_{i+1}&:=x_i\times r &&[x_{i+1}=x_i r]. \end{aligned}

两种迭代方法彼此相关。将 Newton 迭代展开,直接用 bb 表示各项,而不递归引用 xix_i,可得(见习题 J.22):

xi+1=x0(2−x0b)[1+(x0b−1)2][1+(x0b−1)4]⋯[1+(x0b−1)2i]. x_{i+1}=x_0(2-x_0b)[1+(x_0b-1)^2][1+(x_0b-1)^4]\cdots[1+(x_0b-1)^{2^i}].

它与式 J.6.3 很相似。事实上,将式 J.6.3 中的 a,ba,b 替换成 ax0,bx0ax_0,bx_0,并取 a=1a=1,两式完全相同。因此,若以无限精度迭代,两种方法会产生完全相同的 xix_i 序列。

迭代的优点是无需专用除法硬件,可借用乘法器,只需增加控制逻辑;而且每步使正确位数翻倍,普通除法则每步只增加固定数量的位。

迭代求倒数有两个缺点。首先,IEEE 要求除法正确舍入,迭代却只能保证接近正确舍入值。Newton 方法先算 1/b1/b 再乘 aa,还存在额外问题:倒数正确舍入,并不保证商正确舍入。例如,十进制 p=2p=2,a=13,b=51a=13,b=51,则 a/b=.2549…a/b=.2549\ldots 应舍入为 .25;但 1/b=.0196…1/b=.0196\ldots 舍入为 .020,再乘 13 得 .26,末位相差 1。其次,迭代不给出余数;若借用浮点除法硬件做整数除法,这尤其麻烦,因为几乎所有高级语言都有余数运算。

传统经验认为,若想通过迭代得到正确舍入结果,应先将 1/b1/b 算到略多于 2p2p 位,再将 a/ba/b 算到略多于 2p2p 位,最后舍入到 pp 位。但还有更快的方法,似乎最早实现于 TI 8847:仅增加约 6 位精度,算出初步商 qq;再以同样额外 6 位精度比较 qbqb 与 aa,就能迅速决定 qq 是否正确舍入,或者最低位应加 1、减 1。习题 J.21 进一步讨论。

选择算法时,还要考虑除法与乘法的相对速度。除法更复杂,通常更慢。常见经验目标是使除法速度达到乘法的约三分之一。支持这一目标的理由之一,是一些实际程序,例如部分版本的 spice,除法与乘法次数之比为 1:3。另一个“3”来自平方根迭代:每次一次除法的算法可以换成每次三次乘法,见习题 J.20。

浮点余数

对于非负整数,整数除法与余数满足

a=(aDIV⁡b)b+aREM⁡b,0≤aREM⁡b<b. a=(a\operatorname{DIV}b)b+a\operatorname{REM}b, \qquad0\le a\operatorname{REM}b<b.

类似地,可定义浮点余数为 x=INT⁡(x/y)y+xREM⁡yx=\operatorname{INT}(x/y)y+x\operatorname{REM}y。怎样把 x/yx/y 转为整数?IEEE 余数采用就近取偶规则:选择满足 ∣x/y−n∣≤1/2|x/y-n|\le1/2 的整数 nn;若有两个,取偶数。然后定义 REM⁡=x−yn\operatorname{REM}=x-yn。与整数余数范围不同,浮点余数满足 ∣xREM⁡y∣≤y/2|x\operatorname{REM}y|\le y/2。这是精确定义,却不适合直接执行,因为 nn 可能极大;单精度中可达 2127/2−126=2253≈10762^{127}/2^{-126}=2^{253}\approx10^{76}。

译注:这里余数界采用正除数 yy;对一般非零实数除数,应写为 ∣y∣/2|y|/2。

若采用直接除法,求 REM 有自然办法:按计算 x/yx/y 的过程进行。设 x=s12e1,y=s22e2x=s_1 2^{e_1},y=s_2 2^{e_2},在图 J.2(b) 中将 s1s_1 装入 P,s2s_2 装入 B。执行 e1−e2e_1-e_2 次除法步骤后,P 保存 r=x−ynr=x-yn,满足 0≤r<y0\le r<y。IEEE 余数绝对值不超过 y/2y/2,所以结果是 rr 或 r−yr-y。只需保存最后一个商位,以处理恰好位于中点的情况。

但指数差可能很大,而浮点单元通常限制一条指令的最长执行时间,因此往往不能直接实现 REM。J.10 节的芯片都没有 REM,不过可像 Intel 8087 系列那样提供 remainder-step 指令:接收 x,yx,y,不断执行除法步骤,直到 P 中得到余数,或已执行 nn 步;nn 是较小的常数,例如最高支持精度的除法步数。这样,软件可重复调用 REM-step,第一次用 xx 作分子,之后改用上一次的部分余数。原书给出的调用次数是 ⌊(e1−e2)/n⌋\lfloor(e_1-e_2)/n\rfloor。

译注:原页确实使用向下取整符号。但“每次至多 n 步、反复执行直到完成”通常还需计入不足 n 步的最后一次,不能把该式直接当作所有输入的精确调用次数。

REM 可用于三角函数。为简单起见,设十进制五位有效数字,计算 sin⁡x\sin x。若 x=7x=7,可用 π≈3.1416\pi\approx3.1416 缩减参数,算 sin⁡(7−2×3.1416)=sin⁡(.7168)\sin(7-2\times3.1416)=\sin(.7168)。但若 x=2.0×105x=2.0\times10^5,则 2×105/3.1416=63661.82\times10^5/3.1416=63661.8,五位系统得到 63662;再乘 3.1416,精确积为 200000.5392,却舍入为 2.0000×1052.0000\times10^5,参数缩减竟得到 0,远非正确结果。问题是精度不足。

若有 REM,可算 2×105REM⁡3.1416=−.539202\times10^5\operatorname{REM}3.1416=-.53920,但仍不准确,因为 3.1416 只是 π\pi 的近似值;实际 2×105REM⁡π=−.071513…2\times10^5\operatorname{REM}\pi=-.071513\ldots。

传统上,计算大参数周期函数有两种做法:参数过大时报错,或者保存很多位 π\pi,作精确参数缩减。REM 对这两者帮助都不大。某些数学库,例如 Berkeley UNIX 4.3bsd,使用第三种办法:把 π\pi 舍入为最近浮点数,称为“机器 π\pi”,记作 π′\pi',然后用 xREM⁡π′x\operatorname{REM}\pi' 缩减参数。大 xx 时它与 xREM⁡πx\operatorname{REM}\pi 差别很大,因此这样计算并不能得到真实的 sin⁡x\sin x。不过,以这种方式计算各三角函数,可使 sin⁡2x+cos⁡2x=1\sin^2x+\cos^2x=1 等熟悉恒等式在几个舍入误差范围内仍成立。因此,REM 配合机器 π\pi 提供了一种简单办法:小参数时准确,大参数时仍可能有用。

参数缩减时,若 REM 还能返回 nn 的低位,其中 xREM⁡y=x−nyx\operatorname{REM}y=x-ny,就很方便。实际实现往往用小于 2π2\pi 的量,例如 π/2\pi/2,利用 sin⁡(x−π/2)=−cos⁡x\sin(x-\pi/2)=-\cos x、sin⁡(x−π)=−sin⁡x\sin(x-\pi)=-\sin x 等恒等式;这时需要 nn 的低位来选择正确公式。