计算机整数运算

2.3 整数运算

接下来,我们讲一讲整数的运算,在本章刚开始,就举过几个例子,说明了计算机的整数和现实中的整数是不完全一样的,运算也是一样。在运行C程序的时候,有时候会发现一些奇怪的结果,比如两个正数相加,结果竟然会是个负数。这种情况我们称之为溢出(overflow)

2.3.1 无符号加法

对于两个w位的无符号数x和y,有:
0 ≤ x < 2 w , 0 ≤ y < 2 w 0 ≤ x + y < 2 w + 1 0\le x < 2^w, 0\le y < 2^w \\ 0 \le x+y < 2^w+1 0x<2w0y<2w0x+y<2w+1
可以看到两个数相加后的值可能会超过 2 w 2^w 2w ,也就是说,这个数并不能被表示为w位的无符号数,这样溢出就产生了。但是我们又需要将其放入w位中,也就是2.2.7节中所说的截断,所以实际的结果会是 ( x + y ) m o d 2 w (x+y)mod2^{w} (x+y)mod2w,如果画出来的话就是这个样子的,会存在一个断崖,因为超过后就会取模,然后又变回0,再继续增加。

image-20220112202128942

换个说法,也就是说当加法的结果大于等于 2 w 2^w 2w,那么实际得到的和就会变成 x + y − 2 w x+y-2^w x+y2w

通过上面的这些结论,我们也可以得到检测无符号数加法溢出的算法:如果两个无符号数的和比其中一个要小(小于x或者小于y)那么就发生了溢出。证明也很简单,如果没有发生溢出,那么肯定和比其中任何一个加数都要大;如果发生了溢出,那么加法的和 s = x + y − 2 w s=x+y-2^w s=x+y2w,并且 y < 2 w y<2^w y<2w,那么 y − 2 w < 0 y-2^w<0 y2w<0,于是 s < x s<x s<x

2.3.2 补码加法

如果x,y是w位的有符号数,那么有:
− 2 w − 1 ≤ x < 2 w − 1 , − 2 w − 1 ≤ y < 2 w − 1 − 2 w ≤ x + y < 2 w -2^{w-1}\le x < 2^{w-1},-2^{w-1}\le y < 2^{w-1} \\ -2^w\le x+y < 2^w 2w1x<2w12w1y<2w12wx+y<2w
同样,也有可能发生溢出。但是这里的溢出就要分为两类了——正溢出负溢出

对于正溢出而言,就是两个正数相加,然后超过了正数的范围,这时候,进位会被加到符号位,让符号位从0变成1,于是变成了一个负数,也就是本章开始说的那种情况。

正溢出的时候,符号位从0变成1,对于无符号数而言,这个1代表了 2 w − 1 2^{w-1} 2w1,但是实际上变成了 − 2 w − 1 -2^{w-1} 2w1,于是最后的结果相当于是 x + y − 2 w x+y-2^w x+y2w

负溢出也就是从1变成了0,和上面是反的,会变成一个正数,最后的结果会变成 x + y + 2 w x+y+2^w x+y+2w

用图来表示补码加法的结果就是下面这样的:

image-20220112204723714

2.3.3 补码的非

补码的非基本也就是我们所说的相反数,说基本的原因是,有一个个例,就是 T M i n TMin TMin,因为对于 T M i n TMin TMin 而言有下面的式子:
− T M i n = T M i n -TMin=TMin TMin=TMin
所以,如果说对于一个不为0的有符号整数,不一定会有 x ! = − x x!=-x x!=x,例子就是 T M i n TMin TMin

2.3.4 无符号乘法

对于两个w位的无符号数x,y,如果做乘法也会产生溢出,分析和上面无符号数加法类似,有下面的结论:
x ∗ w u y = ( x y ) m o d 2 w x*_w^u y=(xy)mod2^w xwuy=(xy)mod2w

2.3.5 补码乘法

对于两个w位的有符号数x,y,乘法的溢出和上面补码加法的分析类似,有下面的结论:
x ∗ w u y = U 2 T w ( ( x y ) m o d 2 w ) x*_w^u y=U2T_w((xy)mod2^w) xwuy=U2Tw((xy)mod2w)
也就是先当成无符号数做乘法,然后再截断,转化位补码。

2.3.6 乘以常数

在大多数机器上,乘法是很慢的,比加减法、位级运算和移位要慢得多,因此,编译器会进行优化,而优化的方式就是用移位和加法的组合来替代乘以常数的乘法运算。对于计算机而言,要做移位操作是很快的,而移位操作相当于是乘以2的整数幂或者除以2的整数幂。比如左移2位,相当于是乘以 2 2 2^2 22,而右移2位,则相当于是整除 2 2 2^2 22

举个例子,比如要将一个数乘以6,那么实际上可以分别将其乘以2和4,也就是左移1位和2位,然后将这两个结果相加,也就是将它乘以6了。下面有补充的两个例子:

u << 3	==	u * 8
(u << 5)(u << 3)	==	u * 24

2.3.7 除以常数

对于机器而言,乘法以及很慢了,除法就更慢了,乘法如果需要3个时钟周期,那么除法可能需要30个甚至更多的时钟周期,我们同样可以采用移位的方式来加快它的速度。对于乘法而言,移位是向左的,而对于除法,如果是无符号数的,采用逻辑右移即可,如果是有符号数的,则需要使用算数移位。

除以2的整数幂的无符号除法:
若 0 ≤ k < x , 则 x > > k = ⌊ x / 2 k ⌋ 若0 \le k<x,则 x>>k=\lfloor x/2^k \rfloor 0k<x,x>>k=x/2k
除以2的整数幂的补码除法:

对于补码而言稍微要麻烦一些,因为即便使用算数右移,那么得到的结果也是向下舍入的,但是对于负数而言,我们需要的结果是向0舍入,比如-3.14,我们总是向上舍入为-3。但是观察下面的例子,我们就可以看到,算数右移的时候我们总是向下舍入的。

image-20220112232029927

于是,我们通常通过加上一个偏置,来修正这种舍入。

原理:
对 于 整 数 x 和 y ( y > 0 ) , ⌈ x / y ⌉ = ⌊ ( x + y − 1 ) / y ⌋ 对于整数x和y(y>0),\lceil x/y\rceil =\lfloor(x+y-1)/y\rfloor xy(y>0)x/y=(x+y1)/y
于是,利用上面的性质,我们可以使用如下式子来进行负数的除法:
( x + ( 1 < < k ) − 1 ) > > k = ⌈ x / 2 k ⌉ (x+(1<<k)-1)>>k=\lceil x/2^k\rceil (x+(1<<k)1)>>k=x/2k
那么总结一下,我们可以使用一个C表达式来进行补码的除法 x / 2 k x/2^k x/2k

(x < 0 ? (x + (1<<k) - 1) : x) >> k

2.3.8 关于整数的总结

最后,我们思考几个问题。

首先,为什么需要无符号整数,而不是全都用补码呢,这是因为有的时候,我们确实不需要补码,因为没有负数,而使用无符号整数就可以使得表示的范围扩大两倍,因为C保留了无符号整数。

但是,在使用无符号整数的时候会出现很多bug,这是因为默认是使用补码,而补码和无符号在一起运算的时候会进行转换,这些错误又是很难被发现的。

比如下面这样一个程序:

unsigned i;
for (i = cnt-2; i >= 0; i--)
  a[i] += a[i+1];

思考一下,会发生什么结果呢,很容易发现,这个循环永远不会停止,因为i是个无符号整数,当它减到0然后再减1,又变成了 U M a x UMax UMax ,然后用a做引用的时候,就有可能产生分段错误,因为 U M a x UMax UMax 太大了,超出了引用的范围。

本篇文章内容均来自CSAPP导读第2章,希望更加深入了解计算机系统的朋友可以点击看一看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值