2.3 整数运算

2.3.1 无符号加法

  考虑两个非负整数x和y,满足0<=x,y<2w-1。每个数都能表示为w位无符号数字。然而,如果计算它们的和,我们就有一个可能的范围0<=x+y<=2w+1-2。表示这个和可能需要w+1位。例如,图示展示了当x和y有4位表示时,函数x+y的坐标图。参数(显示在水平轴上)的取值范围为0~15,但是和的取值范围为0~30。如果保持和为一个w+1位的数字,并且把它加上另外一个数值,我们可能需要w+2个位,以此类推。这种持续的“字长膨胀”意味着,要想完整的表示算术运算的结果,我们不能对字长做任何限制。一些编程语言,例如Lisp,实际上就支持无限精度的运算,允许任意的(在机器的内存限制内)整数运算。更常见的是,编程语言支持固定精度的运算,因此像“加法”和“乘法”这样的运算不同于它们在整数上的相应运算。

 

  让我们为参数x和y定义运算,其中0<=x,y<2w,该操作是把整数和x+y截断为w位得到的结果,再把这个结果看做是一个无符号数。这可以被视为一种形式的模运算,对x+y的位级表示,简单丢弃任何权重大于2w-1的位就可以计算出和模2w。比如,考虑一个4位数字表示,x=9和y=12的位表示分别为[1001]和[1100]。它们的和是21,5位的表示为[10101]。但是如果丢弃最高位,我们就得到[0101],也就是说,十进制值的5。这就和值21mod16=5一致。

  

  说明公式两种情况,左边的和x+y映射到右边的无符号w位的和x+。正常情况下x+y的值保持不变,而溢出情况则是该和数减去2w的结果。

   推导:无符号数加法

  一般而言,我们可以看到。如果 x+y<2w,和的w+1位表示中的最高位会等于0,因此丢弃它不会改变这个数值。另一方面,如果2w<=x+y<2w+1,和的w+1位表示中的最高位会等于1,因此丢弃它就相当于从和中减去了2w

  当执行C程序是,不会将溢出作为错误而发信号。不过有的时候,我们可能希望判定是否发生了溢出。

  原理:检测无符号数加法中的溢出

  对在范围0<=x,y<=UMaxw中的x和y,令s=x+。则对计算s,当且仅当s<x(或者等价的s<y)时,发生了溢出。 

  作为说明,在前面的示例中,我们看到9+412=5。由于5<9,我们可以看出发生了溢出。

2.3.2 补码加法

  对于补码加法,我们必须确定当结果太大(为正)或者太小(为负)时,应该做些什么。给定在范围-2w-1<=x,y<2w-1-1之内的整数值x和y,它们的和范围-2w<x+y<2w-2之内,要想准备表示,可能需要w+1位。我们仍通过将表示截断到w位,来避免数据大小的不断扩张。然而,结果却不像模数加法那样在数学上感觉很熟悉。定义x+为整数和x+y被截断为w位的结果,并将这个结果看做是补码数。 

 

 

 

 

  当和x+y超过TMaxw时,我们说发生了正溢出。在这种情况下,截断的结果是从和数中减去2w。当和x+y小于TMinw时,我们说发生了正溢出。在这种情况下,截断的结果是把和数加上2w

  两个数的w位补码之和与无符号之和有完全相同的位级表示。实际上,大多数计算机使用同样的机器指令来执行无符号或者有符号加法。

2.3.3 补码的非

  我们看到范围在TMinw<=x<=TMaxw中的每个数字x都有下的加法逆元,我们将表示如下。

  

 

   也就是说,对w位的补码加法来说,Tminw是自己的加法的逆,而对其他任何数值x都有-x作为其加法的逆。

  推导:补码的非

  观察发现TMinw+TMinw = -2w-1+(-2w-1)=-2w。这就导致负溢出,因此TMinw+=-2w+2w=0。对满足x>TMinw的x,数值-x可以表示为一个w位的补码,它们的和-x+x=0。 

 2.3.4 无符号乘法

  范围在0 <=x,y<=2w-1内的整数x和y可以被表示为w位的无符号数,但是它们的乘积x*y的取值范围为0到(2w-1)2=22w-2w+1+1之间。这可能需要2w位来表示。不过,C语言中的无符号乘法被定义为产生W位的值,就是2W位的整数乘积的低w位表示的值。

  将一个无符号数截断为w位等价于计算该值模2w,得到:

   

 2.3.5 补码乘法

  范围在-2w-1<=x,y<=2w-1-1内的整数x和y可以被表示为w位的补码数字,但是它们的乘积x*y的取值范围为-2w-1*(2w-1-1)=-22w-2+2w-1到-2w-1 *-2w-1  = -22w-2之间。要想用补码来表示这个乘积,可能需要2w位。然而,C语言中的有符号乘法是通过将2w位的乘积截断为w位来实现的。我们将这个数值表示为。将一个补码数截断为w为相当于先计算该值模2w,再把无符号数转换为补码,得到:

 

 

 2.3.6 乘以常数

  以往,在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其他整数运算(例如加法、减法、位级运算和移位)只需要一个时钟周期。即使在Inter Core i7上,其整数乘法也需要三个时钟周期。因此,编译器使用了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。首先,我们会考虑乘以2的幂的情况,然后再概况成乘以任意常数。

   

   因此,比如,当w=4时,11可以被表示为[1011]。k=2时将其左移得到6位向量[101100],即可编码为无符号数11*4=44。

  注意,无论是无符号运算还是补码运算,乘以2的幂都可能会导致溢出。结果表明,即使溢出的时候我们通过移位得到的结果也是一样的,如上例,我们将4位模式[1011](数值11)左移两位得到[101100](数值44)。将这个值截断为4位得到[1011](数值为12=44mod16)。

  由于整数乘法比移位和加法的代价要大得多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数常数的情况。例如,假设一个程序包含表达式x*14。利用14=23+22+21,编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1),将一个乘法替换为三个移位和两个加法。无论x是无符号的还是补码,甚至当乘法会导致溢出时,两个计算都会得到一样的结果。(根据整数运算的熟悉可以证明)。更好的是,编译器还可以利用属性14=24-21,将乘法重写为(x<<4)-(x<<1),这时只需要两个移位和一个减法。

2.3.7 除以2的幂

  在大多数机器上,整数除法要比整数乘法更慢--需要30个或者更多的时钟周期。除以2的幂也可以用移位运算来实现。只不过用的是右移,而不是左移。无符号和补码数分别使用逻辑移位和算术移位来达到目的。

2.3.8 关于整数运算的思考

  计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。我们还看到,补码表示提供了一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括像加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补码形式表示的,都有完全一样或者非常类似的位级行为。

  我们看到了C语言中的某些规定可能会产生令人意想不到的结果,而这些结果可能是难以察觉或理解的缺陷的源头。我们特别看待了unsigned数据类型,虽然它概念上很简单,但可能导致即使资深程序员都意想不到的行为。

posted @ 2020-05-08 23:09  风墓  阅读(436)  评论(0编辑  收藏  举报