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
0≤x<2w,0≤y<2w0≤x+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,再继续增加。
换个说法,也就是说当加法的结果大于等于 2 w 2^w 2w,那么实际得到的和就会变成 x + y − 2 w x+y-2^w x+y−2w。
通过上面的这些结论,我们也可以得到检测无符号数加法溢出的算法:如果两个无符号数的和比其中一个要小(小于x或者小于y)那么就发生了溢出。证明也很简单,如果没有发生溢出,那么肯定和比其中任何一个加数都要大;如果发生了溢出,那么加法的和 s = x + y − 2 w s=x+y-2^w s=x+y−2w,并且 y < 2 w y<2^w y<2w,那么 y − 2 w < 0 y-2^w<0 y−2w<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
−2w−1≤x<2w−1,−2w−1≤y<2w−1−2w≤x+y<2w
同样,也有可能发生溢出。但是这里的溢出就要分为两类了——正溢出和负溢出。
对于正溢出而言,就是两个正数相加,然后超过了正数的范围,这时候,进位会被加到符号位,让符号位从0变成1,于是变成了一个负数,也就是本章开始说的那种情况。
正溢出的时候,符号位从0变成1,对于无符号数而言,这个1代表了 2 w − 1 2^{w-1} 2w−1,但是实际上变成了 − 2 w − 1 -2^{w-1} −2w−1,于是最后的结果相当于是 x + y − 2 w x+y-2^w x+y−2w。
负溢出也就是从1变成了0,和上面是反的,会变成一个正数,最后的结果会变成 x + y + 2 w x+y+2^w x+y+2w。
用图来表示补码加法的结果就是下面这样的:
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
x∗wuy=(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)
x∗wuy=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
若0≤k<x,则x>>k=⌊x/2k⌋
除以2的整数幂的补码除法:
对于补码而言稍微要麻烦一些,因为即便使用算数右移,那么得到的结果也是向下舍入的,但是对于负数而言,我们需要的结果是向0舍入,比如-3.14,我们总是向上舍入为-3。但是观察下面的例子,我们就可以看到,算数右移的时候我们总是向下舍入的。
于是,我们通常通过加上一个偏置,来修正这种舍入。
原理:
对
于
整
数
x
和
y
(
y
>
0
)
,
⌈
x
/
y
⌉
=
⌊
(
x
+
y
−
1
)
/
y
⌋
对于整数x和y(y>0),\lceil x/y\rceil =\lfloor(x+y-1)/y\rfloor
对于整数x和y(y>0),⌈x/y⌉=⌊(x+y−1)/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章,希望更加深入了解计算机系统的朋友可以点击看一看。