第17章 扩展精度算术

在整数位宽为32位的计算机上,能够表示从-2 147 483 648~+2 147 483 647的有符号整数(使用二进制补码表示),以及从0~4 294 967 295的无符号整数。对很多(可能是大多数)应用程序来说,上述范围已经足够大了,但有一些应用程序需要更大的表示范围。整数的表示范围相对较小,但可以表示其中每一个整数值。浮点数的表示范围很巨大,但只能表示其中相对较少的值。如果对精确值取近似是可接受的,那么可以使用浮点数,例如许多科学应用,但在需要使用一个很大的范围中所有的整数值时,就不能使用浮点数了。

本章描述了一个很底层的接口XP,它导出了一些函数,可用于固定精度扩展整数的算术操作。可以表示的值只受限于可用的内存。该接口用来服务于较高级的接口,如下两章描述的接口。这些高级接口的设计,使之可用于需要巨大范围整数值的应用程序中。

17.1 接口

一个n个数位的无符号整数x可以表示为下述多项式:

x=xn-1 bn-1 +xn-2 bn-2 +…+x1 b1 +x0

其中b为基数,0≤xi <b。在无符号整数位宽32位的计算机上,n为32,b为2,每个系数xi 表示为(32个比特位中)对应的比特位。这种表示可以推广,用于以任意基数来表示无符号整数。例如,如果b为10,那么每个xi 是0~9(含)的一个整数,x可以表示为一个数组。数字2 147 483 647可以表示为下列数组

unsigned char x[] = { 7, 4, 6, 3, 8, 4, 7, 4, 1, 2 };  

其中xi 保存在x[i]中。数位xi 在x中出现的顺序,是最低位优先,这是实现算术操作最方便的顺序。

选择较大的基数可以节省内存,因为基数越大,数位的范围越大。例如,如果b为216 =65 536,每个数位是一个0~65 535(含)的数,只需要两个数位(4个字节)即可表示2 147 483 647:

unsigned short x[] = { 65535, 32767 };  

而以下包含64个数位的十进制数

349052951084765949147849619903898133417764638493387843990820577

可以表示为一个14个元素(28个字节)的数组:

{ 38625,  9033, 28867,  3500, 30620, 54807, 4503, 
  60627, 34909, 43799, 33017, 28372, 31785,  8 }.  

如果b为2k 而k是C语言中某种预定义无符号整数类型的位宽,那么可以使用较小的基数而不会浪费空间。可能更重要的一点是,较大的基数会使某些算术操作的实现复杂化。如下文详述,如果unsigned long类型可以容纳b3 -1,那么即可避免这种复杂化。XP使用的b值为28 ,将每个数位存储在一个无符号字符中,因为标准C语言保证unsigned long位宽至少为32,其中至少包含3个字节,因此unsigned long可以容纳b3 -1=224 -1。使用b=28 ,需要花费4个字节表示2 147 483 647:

unsigned char x[] = { 255, 255, 255, 127 };  

需要27个字节表示上述的64个数位的十进制数:

{ 225, 150, 73, 35, 195, 112, 172,  13, 156, 119, 23, 214, 151, 17, 
  211, 236, 93, 136, 23, 171, 249, 128, 212, 110, 41, 124,  8 }.  

XP接口揭示了这些表示细节:

xp.h

〉≡   
  #ifndef XP_INCLUDED  
  #define XP_INCLUDED  
 
  #define T XP_T  
  typedef unsigned char *T;  
 
 〈exported functions

 214〉  
 
  #undef T  
  #endif  

即XP_T是一个由无符号字符构成的数组,包含了一个n位数的的各个数位,基数为28 ,最低位优先。

如下所述,XP接口中的函数以n为输入参数,XP_T实例为输入/输出参数,这些数组必须足够大以便容纳n个数位。向该接口中任何函数传递的XP_T实例为NULL、XP_T实例容量太小、或长度n不是正值,都是未检查的运行时错误。XP是一个危险的接口,因为省略大部分已检查的运行时错误。这种设计有两个原因。XP的目标客户程序是较高级的接口,这些接口很可能已经规定并实现了必要的已检查的运行时错误。其次,XP接口要尽可能简单,以便将其中一些函数以汇编语言实现(如果有性能方面的要求)。后一种考虑,是XP函数不进行内存分配的原因。

以下函数

exported functions

 214〉≡  
  extern int XP_add(int n, T z, T x, T y, int carry);  
  extern int XP_sub(int n, T z, T x, T y, int borrow);  

实现了z=x+y+carry和z=x-y-borrow。在此处以及下文,x、y和z指代由数组x、y和z表示的整数值,假定这些整数值包含n个数位。carry和borrow必须为0或1。XP_add将z[0..n-1]设置为x+y+carry的值(和值最多包含n个数位),并返回最高有效位的进位输出。XP_sub将z[0..n - 1]设置为x-y-borrow的值(差值最多n个数位),并返回最高有效位的借位输出。因而,如果XP_add返回1,则n个数位无法容纳x+y+carry的值,而如果XP_sub返回1,那么y>x。如果只考虑这两个函数,x、y或z中任意多个参数,均可为同一XP_T实例。

exported functions

 214〉+≡  
  extern int XP_mul(T z, int n, T x, int m, T y);  

上述函数实现了z=z+x*y,其中x有n个数位,y有m个数位。z必须足以容纳n+m个数位:XP_mul将n+m个数位的乘积x*y,加到z上。当z初始化为0时,XP_mul将z[0..n+m-1]设置为x*y。XP_mul的返回值,是x和y的乘积最高有效位的进位输出。如果z与x或y为同一XP_T实例,则造成未检查的运行时错误。

XP_mul说明了const限定符可以发挥作用的情形,const有助于标识输入/输出参数,还可以作为文档,以防止此类运行时错误。下述声明

extern int XP_mul(T z, int n, const unsigned char *x,  
                       int m, const unsigned char *y);  

明确地规定了XP_mul从x和y读取并写入到z,因而隐含地指出了z不应该与x或y相同。对x和y不能使用const T的语法,因为这将意味着“指向unsigned char的常数指针”,而不是我们预期的“指向unsigned char常数的指针”(参见2.4节)。习题19.5探讨了能够与const限定符正常协作的一些其他形式的T定义。

但const限定符并不能防止同一XP_T实例分别作为x和z(或y和z)传递,因为unsigned char *类型的值可以传递给const unsigned char *类型的参数。但const的这种用法,确实允许将一个const unsigned char *类型的值作为x和y传递,在XP接口声明的上述XP_mul函数中,必须使用类型转换来传递这些值。在XP接口中,const的少量好处,很难平衡其冗长啰嗦的缺点。

以下函数

exported functions

 214〉+≡  
  extern int XP_div(int n, T q, T x, int m, T y, T r,T tmp);  

实现了除法:它计算了q=x/y和r=x mod y,q和x有n个数位,r和y有m个数位。如果y为0,XP_div返回0,不改变q和r,否则,它将返回1。tmp必须能够容纳至少n+m+2个数位。q或r与x和y中之一相同、q和r是同一XP_T实例、tmp能够容纳的数位太少,都是未检查的运行时错误。

以下函数

exported functions

 214〉+≡  
  extern int XP_sum     (int n, T z, T x, int y);  
  extern int XP_diff    (int n, T z, T x, int y);  
  extern int XP_product (int n, T z, T x, int y);  
  extern int XP_quotient(int n, T z, T x, int y);  

实现了n个数位的XP_T实例x和单个数位的整数值y(基数28 )之间的加法、减法、乘法和除法。XP_sum将z[0..n - 1]设置为x+y的值,并返回最高有效位的进位输出。XP_diff将z[0..n - 1]设置为x-y的值,并返回最高有效位的借位输出。对于XP_sum和XP_diff,y必须为正数且不能大于基数28

XP_product将z[0..n - 1]设置为x*y的值,并返回最高有效位的进位输出,进位最大为28 -1。XP_quotient将z[0..n - 1]设置为x/y的值并返回余数x mod y,余数最大为y-1。对于XP_product和XP_quotient,y不能大于28 -1。

exported functions

 214〉+≡  
  extern int XP_neg(int n, T z, T x, int carry);  

该函数将z[0..n - 1]设置为~x + carry的值,并返回最高有效位的进位输出。在carry为0时,XP_neg实现了取反(one's complement negation),在carry为1时,XP_neg实现了求补(two's complement negation)。

XP_T实例通过下列函数比较

exported functions

〉+≡  
  extern int XP_cmp(int n, T x, T y);  

对于x<y、x=y或x>y三种情形,该函数分别返回负值、0、正值。

可以用下列函数对XP_T进行移位操作:

exported functions

 214〉+≡  
  extern void XP_lshift(int n, T z, int m, T x,  
      int s, int fill);  
  extern void XP_rshift(int n, T z, int m, T x,  
      int s, int fill);  

上述两个函数分别将x左移/右移s个比特位得到的值赋值给z,其中z有n个数位,x有m个数位。当n大于m时,x高位缺失的那些数位,如果进行左移,则其中的比特位当做0处理,如果进行右移,其中的比特位当做fill处理。空出的比特位用fill填充,fill必须为0或1。fill为0时,XP_rshift实现了逻辑右移(logical right shift),fill为1时,XP_rshift可用于实现算术右移(arithmetic right shift)。

exported functions

 214〉+≡  
  extern int           XP_length (int n, T x);  
  extern unsigned long XP_fromint(int n, T z,  
      unsigned long u);  
  extern unsigned long XP_toint  (int n, T x);  

XP_length返回x中数位的数目,即,它返回x[0..n-1]中最高非零数位的索引加1。XP_fromint将z [ 0..n – 1]设置为u mod 28n 并返回u/28n ,即返回u中z无法容纳的那些比特位。XP_toint返回x mod (ULONG_MAX+1),即x中最低8 * sizeof(unsigned long)个比特位。

剩余的XP函数负责在字符串和XP_T之间进行双向转换。

exported functions

 214〉+≡  
  extern int XP_fromstr(int n, T z, const char *str,  
      int base, char **end);  
  extern char *XP_tostr  (char *str, int size, int base,  
      int n, T x);  

XP_fromstr类似C库中的strtoul,它将str中的字符串解释为以base为基数的无符号整数。该函数忽略字符串开头的空白字符,并处理其后以base为基数的一个或多个数位。对于11~36的基数来说,XP_fromstr将小写或大写字母解释为大于九的数位。base小于2或大于36,则为已检查的运行时错误。

计算 str指定的整数时,将使用通常的乘法算法,逐位累积计算,保存至n数位的XP_T实例z:

for (p = str; *p is a digit

; p++)  
    z

 ← base*z

 + *p's value

  

函数的实现中,不会将z初始化为0,客户程序必须正确地初始化z值。在一系列的base * z乘法运算中,当第一次出现非零进位输出时,该进位输出值将用作XP_fromstr的返回值,如果始终都没有非零进位输出,则返回0。因而,如果z无法容纳str指定的数字,则XP_fromstr将返回非零值。

如果end不是NULL,函数会将*end指向XP_fromstr的解释过程结束的那个字符,此时可能发生了乘法上溢或扫描到非数字字符。如果str中的各个字符不是基数base下的整数,那么XP_fromstr将返回0,并将*end设置为str(如果end不是NULL)。str是NULL,则造成已检查的运行时错误。

XP_tostr将x在基数base下的字符表示填充到str中(以0结尾),并返回str。x将设置为零。在base大于10时,大写字母用于表示大于9的数位。base小于2或大于36,则为已检查的运行时错误。str为NULL或size太小,也是已检查的运行时错误,size太小,是指x的字符表示加上一个0字符,超出size个字符的情形。

17.2 实现

xp.c

〉≡  
  #include <ctype.h>  
  #include <string.h>  
  #include "assert.h"  
  #include "xp.h"  
 
  #define T XP_T  
  #define BASE (1<<8)  
 
 〈data

 229〉  
 〈functions

 217〉  

XP_fromint和XP_toint说明了XP函数必须执行的各种算术操作。XP_fromint初始化一个XP_T,使之等于某个指定的unsigned long值:

functions

 217〉≡  
  unsigned long XP_fromint(int n, T z, unsigned long u) {  
      int i = 0;  
 
      do  
          z[i++] = u%BASE;  
      while ((u /= BASE) > 0 && i < n);  
      for ( ; i < n; i++)  
          z[i] = 0;  
      return u;  
  }  

严格来说,u%BASE不是必需的,因为对z[i]的赋值隐含地进行了模操作。所有实现算术操作的XP函数都执行了此类显式操作,以便协助说明函数使用的算法。由于基数是2的常数次幂,大多数编译器会将基数相关的乘法、除法、取模转换为等效的左移、右移、逻辑与。

XP_toint是XP_fromint的逆:它将XP_T的最低8 * sizeof(unsigned long)个比特位当做unsigned long返回。

functions

 217〉+≡  
  unsigned long XP_toint(int n, T x) {  
      unsigned long u = 0;  
      int i = (int)sizeof u;  
 
      if (i > n)  
          i = n;  
      while (--i >= 0)  
          u = BASE*u + x[i];  
      return u;  
  }  

一个非零的n数位XP_T,如果其最高位部分是一个或多个连续的0,那么其有效数位的数目要少于n。XP_length返回有效数位的数目,不计算最高有效位之前的0数位:

functions

 217〉+≡  
  int XP_length(int n, T x) {  
      while (n > 1 && x[n-1] == 0)  
          n--;  
      return n;  
  }  

17.2.1 加减法

实现加减法的算法,实际上是小学里笔算技巧的系统化再现。假定基数为10,下述例子很好地说明了加法z=x+y:

230

加法的过程从最低有效位到最高有效位进行,在本例中,进位值的初始值为0。每一步都建立和值S=carry+xi +yi ,zi 的值为S mod b,新的进位值为S/b,其中b为基数,本例中为10。顶行中以小号字体显示的数字是进位值,底部一行中以两个数位显示的数字是S的值。在本例中,进位输出为1,因为4个数位无法容纳和值。XP_add精确地实现了本算法,并返回最终的进位值:

functions

 217〉+≡   
  int XP_add(int n, T z, T x, T y, int carry) {  
      int i;  
 
      for (i = 0; i < n; i++) {  
          carry += x[i] + y[i];  
          z[i] = carry%BASE;  
          carry /= BASE;  
      }  
      return carry;  
  }  

循环的每一次迭代中,carry暂时保存了对应于当前数位的和值S,而后的除法,使得carry只包含进位值。各个数位都是0和b-1之间的一个数字,进位值可以为0或1,因此对单个数位来说,和值S的最大值为(b-1)+(b-1)+1=2b-1=511,很容易放入一个int值中。

减法z=x-y,类似于加法:

230-2

减法的过程从最低有效位到最高有效位进行,在本例中,借位值的初始值为0。每一步都形成差值D=xi +b-borrow-yi ,zi 的值为D mod b,新的借位值为1-D/b。顶行中以小号字体显示的数字是借位值,底部一行中以两个数位显示的数字是D的值。

functions

 217〉+≡   
  int XP_sub(int n, T z, T x, T y, int borrow) {  
      int i;  
 
      for (i = 0; i < n; i++) {  
          int d = (x[i] + BASE) - borrow - y[i];  
          z[i] = d%BASE;  
          borrow = 1 - d/BASE;  
      }  
      return borrow;  
  }  

D至多为(b-1)+b-0-0=2b-1=511,很容易放入一个int值中。如果最终的借位值非零,那么x小于y。

单数位加减法 [1] 比通用的函数简单些,它们使用第二个操作数作为进位或借位:

functions

 217〉+≡  
  int XP_sum(int n, T z, T x, int y) {  
      int i;  
 
      for (i = 0; i < n; i++) {  
          y += x[i];  
          z[i] = y%BASE;  
          y /= BASE;  
      }  
      return y;  
  }  
  int XP_diff(int n, T z, T x, int y) {  
      int i;  
 
      for (i = 0; i < n; i++) {  
          int d = (x[i] + BASE) - y;  
          z[i] = d%BASE;  
          y = 1 - d/BASE;  
      }  
      return y;  
  }  

XP_neg类似单数位加法,但x的各个数位在加法之前会取反:

functions

 217〉+≡  
  int XP_neg(int n, T z, T x, int carry) {  
      int i;  
 
      for (i = 0; i < n; i++) {  
          carry += (unsigned char)~x[i];  
          z[i] = carry%BASE;  
          carry /= BASE;  
      }                                                    
      return carry;  
  }  

到unsigned char的类型转换确保了~x[i]的值小于b。

17.2.2 乘法

如果x有n个数位而y有m个数位,z=x*y会形成m个部分积,每个部分积都有n个数位,这m个部分积的和有n+m个数位。以下例子说明了当z的初始值为0、n为4、m为3的情形下,乘法执行的过程:

232

部分积不必明确地计算出来,在计算乘积中的的各个数位时,每个部分积都会加到z上。例如,第一个部分积8 * 732中的各个数位,会从最低有效位到最高有效位进行计算。该部分积的第i个数位将加到z的第i个数位,同时还会应用加法中的进位计算。第二个部分积2 * 732的第i个数位,加到z的第i+1个数位。一般来说,当计算涉及xi 的部分积时,该部分积的各个数位将从z的第i个数位开始,加到z上。

functions

 217〉+≡   
  int XP_mul(T z, int n, T x, int m, T y) {  
      int i, j, carryout = 0;  
 
      for (i = 0; i < n; i++) {  
          unsigned carry = 0;  
          for (j = 0; j < m; j++) {  
              carry += x[i]*y[j] + z[i+j];  
              z[i+j] = carry%BASE;  
              carry /= BASE;  
      }  
      for ( ; j < n + m - i; j++) {  
          carry += z[i+j];  
          z[i+j] = carry%BASE;  
          carry /= BASE;  
            }  
            carryout |= carry;  
        }  
        return carryout;  
  }  

因为在第一个嵌套循环中,来自部分积的各个数位加到z上,进位值最大可以达到b-1,因此保存在carry中的和值,最大可以达到(b-1)(b-1)+(b-1)=b2 -b=65 280,unsigned类型完全可以容纳该值。在将一个部分积加到z之后,第二个嵌套循环将进位值加到z中余下的数位上,并记录“这次”加法中z的最高位的进位。如果该进位值为1,则z+x*y的进位输出为1。

单数位乘法相当于XP_mul的特例,即m等于1、z初始化为0的情形:

functions

 217)+≡  
  int XP_product(int n, T z, T x, int y) {  
      int i;  
      unsigned carry = 0;  
 
      for (i = 0; i < n; i++) {  
          carry += x[i]*y;  
          z[i] = carry%BASE;  
          carry /= BASE;  
      }  
      return carry;  
  }  

17.2.3 除法和比较

除法是最复杂的算术函数。有几种算法可以使用,其各有优缺点。可能其中最容易理解的算法,来自于计算q=x/y和r=x mod y的下述数学规则。

if x

 < y

 then q

 ← 0, r

 ← x  
else  
  q′x

/2y

, r′x

 mod 2y

  
  if r′

< y

 then q

 ← 2q′

, rr′

 else q

 ← 2q′

+ 1, rr′

- y

  

当然,涉及q′和r′的中间计算必须使用XP_T完成。

这个递归算法的问题在于对q′和r′的内存分配。这种分配可能多达lg x次(这里,lg是以2为底的对数),因为lg x是递归深度的最大值。XP接口禁止这种隐含的内存分配。

对于x≥y且y至少有两个有效数位的一般情形,XP_div使用了一种高效的迭代算法,对x<y的情形和y只有一个数位的情形,将使用更为简单的算法。

functions

 217〉+≡  
  int XP_div(int n, T q, T x, int m, T y, T r, T tmp) {  
      int nx = n, my = m;  
 
      n = XP_length(n, x);  
      m = XP_length(m, y);  
      if (m == 1) {  
         〈single-digit division

 222〉  
      } else if (m > n) {  
          memset(q, '\0', nx);  
          memcpy(r, x, n);  
          memset(r + n, '\0', my - n);  
      } else {  
         〈long division

 223〉  
      }  
      return 1;  
  }  

XP_div首先检查是否为单数位除法,该情形隐含了对除以零的处理。

单数位除法很容易,因为商的各个数位可以使用C语言中普通的无符号整数除法计算。除法从最高位到最低位进行,进位值的初始值为零。十进制下的9428除以7,即说明了除法涉及的各个步骤:

234

在每一步,部分被除数R=carry*b+xi ,商的数位qi =R/y0 ,新的进位值为R mod y0 。进位值是上图中以小号字体显示的数位。进位值的最终值即为余数。这正是XP_quotient所实现的操作,该函数返回余数:

functions

 217〉+≡   
  int XP_quotient(int n, T z, T x, int y) {  
      int i;  
      unsigned carry = 0;  
 
      for (i = n - 1; i >= 0; i--) {  
          carry = carry*BASE + x[i];  
          z[i] = carry/y;  
          carry %= y;  
      }  
      return carry;  
  }  

R在XP_quotient中赋值给carry,其最大值为(b-1)b+(b-1)=b2 -1=65535,unsigned类型值可以容纳该值。

在XP_div中,调用XP_quotient返回的是r的最低有效位,因此其余数位必须明确设置为0:

single-digit division

 222〉≡  
  if (y[0] == 0)  
      return 0;  
  r[0] = XP_quotient(nx, q, x, y[0]);  
  memset(r + 1, '\0', my - 1);  

在一般情况下,n个数位的被除数除以m个数位的除数,其中n≥m且m>1。在基数10下,将615 367除以296,就说明了除法的计算过程。被除数最高位之前会补一个0数位,使得n大于m:

235

高效地计算商的每个数位qk ,是比较长的除法问题的关键,因为其中的计算涉及m个数位的操作数。

暂且假定我们知道如何计算商的各个数位,那么以下伪代码勾勒出了长除法的一个实现。

rem ← x最高位前补0 
for (k = n - m; k >= 0; k--) {  
  compute qk  
  dq ← y*qk  
  q->digits[k] = qk;  
  rem ← rem - dq*b

k

  
  } 
r ← rem  

rem的初始值等于x,最高位前补0。循环中计算了商的n-m+1个数位,首先将rem的前m+1个数位作为被除数,除以m个数位的除数,计算得到商的最高有效位。在每次迭代结束时,从rem减去qk和y的乘积,这会将rem减少一个数位。对上例来说,n=6,m=3,循环体执行了四次,k值分别为6-3=3、2、1、0。下表列出了每个迭代中k、rem、qk和dq的值。第二列中的下划线标识出了rem中除以y的前缀部分,即296。

235-2

XP_div需要空间来容纳两个临时变量rem和dq的各个数位,它需要为rem分配n+1个字节、需要为dq分配m+1个字节,这是tmp必须至少为n+m+2个字节长的原因。在上述的伪代码框架中填入实际内容,用于长除法的代码块即演变为如下的形式:

long division

 223〉≡   
  int k;  
  unsigned char *rem = tmp, *dq = tmp + n + 1;  
  assert(2 <= m && m <= n);  
  memcpy(rem, x, n);  
  rem[n] = 0;  
  for (k = n - m; k >= 0; k--) {  
      int qk;  
     〈compute

 qk, dq ← y*qk 224〉  
      q[k] = qk;  
     〈rem ← rem - dq*bk



 225〉  
  }  
  memcpy(r, rem, m);  
 〈fill out

 q and

 r with 0s

 224〉  

tmp[0..n]容纳了rem的n+1个数位,而tmp[n+1..n+1+m]容纳了dq的m+1个数位。在tmp[0..k+m]中,总是包含rem的k+m+1个数位。下列代码计算了一个n - m + 1个数位的商,和一个m个数位的余数,q和r中其余的数位必须都设置为0:

fill out

 q and

 r with 0s

 224〉≡  
  {  
      int i;  
      for (i = n-m+1; i < nx; i++)  
          q[i] = 0;  
      for (i = m; i < my; i++)  
          r[i] = 0;  
  }  

到这里,只缺少计算商的各个数位所需的逻辑。一个简单但不当的方法是:将qk的初值设置为b-1,然后在一个循环中,只要y * qk大于rem的前m+1个数位,就将qk减1:

qk = BASE-1;  
dq ← y*qk;  
while (rem[k..k+m] < dq) {  
  qk--;  
  dq ← y*qk;  
}  

这种方法太慢:该循环可能需要b-1次迭代,每个迭代需要m个数位的乘法和m+1个数位的比较。更好的方法是使用通常的整数运算更精确地估计qk的值,并在估计错误时进行校正。实际上,用rem的前三个数位除以y的前两个数位,即可得到对qk的估计值,该值可能是正确的,或者比正确值大1。因而,上述的循环可以替换为一个简单的测试:

compute

 qk, dq ← y*qk 224〉≡  
  {  
      int i;  
      assert(2 <= m && m <= k+m && k+m <= n);  
     〈qk ← y[m-2..m-1]/rem[k+m-2..k+m] 225〉  
      dq[m] = XP_product(m, dq, y, qk);  
      for (i = m; i > 0; i--)  
          if (rem[i+k] != dq[i])  
              break;  
      if (rem[i+k] < dq[i])  
          dq[m] = XP_product(m, dq, y, --qk);  
  }  

上述代码块使用XP_product计算y[0..m - 1] * qk,将结果赋值给dq,返回最终的进位值,即dq的最高一个数位。for循环逐数位比较rem[k..k+m]和dq。如果dq大于rem的前m+1个数位,则qk比实际的正确值大1,所以将qk减1并重新计算dq。

可以利用普通的整数除法来估算qk:

〈qk ← y[m-2..m-1]/rem[k+m-2..k+m] 225〉≡  
  {  
      int km = k + m;  
      unsigned long y2 = y[m-1]*BASE + y[m-2];  
      unsigned long r3 = rem[km]*(BASE*BASE) +  
          rem[km-1]*BASE + rem[km-2];  
      qk = r3/y2;  
      if (qk >= BASE)  
          qk = BASE - 1;  
  }  

r3最大为(b-1)b2 +(b-1)b+(b-1)=b3 -1=16777215,unsigned long类型可以容纳r3。这个计算,实际上限制了对BASE值的选择。unsigned long可以容纳小于232 的值,这要求b3 -1<232 ,因此BASE必须小于210.6666 ,即BASE不能大于1625。在2的各个幂中,256是不大于1625的最高次幂,而且刚好是另一个内建类型(unsigned char)所能容纳的最大值。

解决长除法问题,最后一步是从rem的前m+1个数位中减去dq,这减小了rem,并使其减少一个数位。在概念上,可以先算出dq左移k个数位后的值,并从rem减去该值,即可完成该减法。上文给出的XP_sub,可用于完成这个减法运算,只需要将指向适当数位的指针传递给XP_Sub即可:

〈rem ← rem - dq*bk



 225〉≡  
  {  
       int borrow;  
       assert(0 <= k && k <= k+m);  
       borrow = XP_sub(m + 1, &rem[k], &rem[k], dq, 0);  
       assert(borrow == 0);  
  }  

<compute qk, dq←y*qk 224>中的代码说明,可以通过从最高有效位开始逐一比较各个数位,来比较两个多数位的数字。XP_cmp刚好是用这个方法来比较两个XP_T参数的:

functions

 217〉+≡  
  int XP_cmp(int n, T x, T y) {  
      int i = n - 1;  
 
      while (i > 0 && x[i] == y[i])  
          i--;  
      return x[i] - y[i];  
  }  

17.2.4 移位

XP的实现中,有两个函数可以将XP_T左移/右移指定数目的比特位。移位s个比特位,通过两步完成:第一步移位8 * (s/8)个比特位,每次移动一个字节,第二步移位剩余的s mod 8个比特位,一次完成。fill设置为全1或全0的字节值(即0xff或0),以便使用该值一次填充一个字节,如下所示。

functions

 217〉+≡  
  void XP_lshift(int n, T z, int m, T x, int s, int fill) {  
      fill = fill ? 0xFF : 0;  
     〈shift left by

 s/8 bytes

 226〉  
      s %= 8;  
      if (s > 0)  
         〈shift

 z left by

 s bits

 227〉  
  }  

图17-1说明了这些步骤,图中原本是一个六个数位的XP_T,包含44个值为1的比特位,在左移13个比特位后,形成了一个八个数位的XP_T,在右侧的浅色阴影标识了移位后空出的比特位,这些将设置为fill。

238

图17-1 左移13个比特位

左移s/8个字节,可以通过下列赋值操作概述。

z[m+(s/8)..n-1] ← 0  
z[s/8..m+(s/8)-1] ← x[0..m-1]  
z[0..(s/8)-1] ← fill.   

第一个赋值操作,将z中不出现(在x左移s/8字节后)的数位清零。在第二个赋值操作中,xi 复制到zi+s/8 ,首先复制最高有效字节;第三个赋值操作,将z的s/8个最低有效字节设置为fill。这些赋值操作都涉足到循环,初始化代码会处理n小于m的情形:

shift left by

 s/8 bytes

 226〉≡  
  {  
      int i, j = n - 1;  
      if (n > m)  
          i = m - 1;  
      else  
          i = n - s/8 - 1;  
      for ( ; j >= m + s/8; j--)  
          z[j] = 0;  
      for ( ; i >= 0; i--, j--)  
          z[j] = x[i];  
      for ( ; j >= 0; j--)  
          z[j] = fill;  
  }  

在第二步中,s已经简化为需要移位的比特位数目。

这种移位等效于将z乘以2s ,然后将z的s个最低有效比特位设置为fill。

shift

 z left by

 s bits

 227〉≡  
  {  
      XP_product(n, z, z, 1<<s);  
      z[0] |= fill>>(8-s);  
  }  

fill是0或0xFF,因此fill >> (8-s)形成了s个填充比特位,可用于字节的最低s个比特位。

右移也使用了一个类似的两步过程:第一步右移s/8个字节,第二步右移余下的s mod 8个比特位。

functions

 217〉+≡  
  void XP_rshift(int n, T z, int m, T x, int s, int fill) {  
      fill = fill ? 0xFF : 0;  
     〈shift right by

 s/8 bytes

 228〉  
      s %= 8;  
      if (s > 0)  
         〈shift

 z right by

 s bits

 228〉  
  }  

将一个六个数位的XP_T(包含44个值为1的比特位)右移13个比特位,到一个八数位的XP_T中,这一过程说明了右移的步骤,如图17-2所示,左侧的浅色阴影同样标识了空出和过多的比特位,这些比特位将设置为fill。

239

图17-2 右移13个比特位

概述右移过程的三个赋值操作如下

z[0..m-(s/8)-1] ← x[s/8..m-1]  
z[m-(s/8)..m-1] ← fill  
z[m..n-1] ← fill.  

第一个赋值操作将xi 复制到zi-s/8 ,首先复制最低有效字节,从字节s/8开始复制。第二个赋值操作将空出的字节设置为fill,第三个赋值操作将z中未出现在x中的数位设置为fill。当然,第二个和第三个赋值操作可以通过同一个循环完成:

shift right by

 s/8 bytes

 228〉≡   
  {  
      int i, j = 0;  
      for (i = s/8; i < m && j < n; i++, j++)  
          z[j] = x[i];  
      for ( ; j < n; j++)  
          z[j] = fill;  
  }  

第二步将z右移s个比特位,等效于将z除以2s

shift

 z right by

 s bits

 228〉≡  
  {  
      XP_quotient(n, z, z, 1<<s);  
      z[n-1] |= fill<<(8-s);  
  }  

表达式fill << (8-s)形成了s个填充比特位,可用于字节的最高s个比特位,可以按位或到z的最高有效字节中。

17.2.5 字符串转换

XP的最后二个函数用于XP_T与字符串的双向转换。XP_fromstr将字符串转换为XP_T,该函数可处理的字符串,首先是可选的空格,后接一个或多个数位(数位值受指定基数的限制,基数的范围在2~36)。对于大于10的基数,用字母来表示大于9的数位。在遇到非法字符或0字符时,或乘法的进位输出非零时,XP_fromstr停止扫描字符串参数。

functions

 217〉+≡  
  int XP_fromstr(int n, T z, const char *str,  
      int base, char **end) {  
      const char *p = str;  
 
      assert(p);  
      assert(base >= 2 && base <= 36);  
     〈skip white space

 229〉  
      if (〈*p is a digit in base

 229〉) {  
          int carry;  
          for ( ; 〈*p is a digit in base

 229〉; p++) {  
              carry = XP_product(n, z, z, base);  
              if (carry)  
                  break;  
              XP_sum(n, z, z, map[*p-'0']);  
              }  
              if (end)  
                  *end = (char *)p;  
              return carry;  
      } else {  
          if (end)  
                  *end = (char *)str;  
          return 0;  
      }  
  }  
 
〈skip white space

 229〉≡  
  while (*p && isspace(*p))  
      p++;  

如果end不是NULL,XP_fromstr将*end设置为指向停止扫描时的字符。

如果c为数位字符,map[c-'0']是对应的数位值,例如,map['F' - '0']为15。

data

 229〉≡  
  static char map[] = {  
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 
     36, 36, 36, 36, 36, 36, 36,  
     10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,  
     23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,  
     36, 36, 36, 36, 36, 36,  
     10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,  
     23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35  
  };  

在ASCII字符'0'和'z'之间,对于少量无效的数位字符c来说,map[c - '0']为36。这样,在以base为基数时,只要map[c - '0']小于base,那么c就是一个合法的数位字符。因而,XP_fromstr可以用下述方式来测试*p是否为数位字符:

〈*p is a digit in base

 229〉≡  
  (*p && isalnum(*p) && map[*p-'0'] < base)  

XP_tostr使用通常的算法来计算x的字符串表示,首先剥离最后一个数位,当然,XP_tostr使用了XP接口中现有的函数来执行计算。

functions

 217〉+≡  
  char *XP_tostr(char *str, int size, int base,  
      int n, T x) {  
      int i = 0;  
 
      assert(str);  
      assert(base >= 2 && base <= 36);  
      do {  
          int r = XP_quotient(n, x, x, base);  
          assert(i < size);  
          str[i++] =  
              "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[r];  
          while (n > 1 && x[n-1] == 0)  
              n--;  
      } while (n > 1 || x[0] != 0);  
      assert(i < size);  
      str[i] = '\0';  
     〈reverse

 str 230〉  
      return str;  
  }  

str中的各个字符是前后反向的,因此XP_tostr在结束前需要将这些字符逆转过来。

reverse

 str 230〉≡  
  {  
      int j;  
      for (j = 0; j < --i; j++) {  
          char c = str[j];  
          str[j] = str[i];  
          str[i] = c;  
      }  
  }  

17.3 扩展阅读

XP中大部分算术函数都直接了当地实现了小学生水平的四则运算算法。[Hennessy and Patterson,1994]中的第4章和[Knuth,1981]中的4.3节都描述了实现算术操作的经典算法。[Knuth,1981]很好地综述了这些算法的悠久历史。

除法的实现比较困难,因为在计算商的各个数位时,有一些强加的约束。XP_div中使用的算法取自[Brinch-Hansen,1994],该论文包含了对“商数位估计值最多只大1”结论的证明。Brinch-Hansen还说明了,可以通过按比例放大操作数,在大多数情况下都可以避免校正qk。按比例放大,只需要一次额外的单数位乘法和除法,但在大多数情况下可以避免(因qk必须减1而导致的)第二次乘法运算。

17.4 习题

17.1 实现递归式除法算法,并对照XP_div中使用的Brinch-Hansen算法,比较算法执行的时间和空间性能。是否在某些情况下,递归算法更可取?

17.2 实现[Hennessy and Patterson,1994]的第4章中描述的“移位相减”式除法算法,并对照XP_div中使用的Brinch-Hansen算法,比较其性能。

17.3 XP接口中的大部分函数,执行时间都与操作数中数位的数目成正比。因而,以216 为基数来表示XP_T,将使这些函数运行速度提高到原来的两倍。但除法有个问题,因为

(216 )3 -1=28 147 497 610 655.

在大多数32位计算机上该值都大于ULONG_MAX,无法使用普通的C语言整数运算(以一种可移植的方式)来估算商的数位。设计一种方法绕过这个问题,使用216 为基数实现XP接口,并测量这种做法带来的好处。这种做法带来了好处,但是否值得为此而增加除法实现的复杂性?

17.4 针对基数232 ,重新完成习题17.3。

17.5 使用更大基数如232 的扩展精度算术,通常更容易用汇编语言实现,因为许多机器提供了双精度指令,通常也很容易获得进位和借位值。而且,汇编语言实现也总是更快。请读者在喜爱的计算机上用汇编语言重新实现XP接口,并测定其在速度方面的改进。

17.6 实现一个XP函数,可以在指定范围内生成均匀分布的随机数。


[1] 有一个操作数只有单个数位。——译者注