CSAPP(二)上——整数表示 信息的表示和处理
整数表示
无符号
无符号整数就是正整数,它很简单,不用考虑负数该如何存储。
如果你用\(w\)位来存储一个无符号整数,如向量\(\vec{x} = [x_{w-1}, x_{w-2},...,x_0]\),那么你可以存储\([0, 2^w - 1]\)个整数。
我们定义一个函数来表示一个位向量B到一个无符号整数U的映射。
有符号
有符号整数就是有正有负的整数。电脑肯定不认识负号,所以需要一种以纯数字的形式来表示正负的手段,补码是其中的一种手段,并且使用最广,下面介绍的都是补码,下面当我说无符号数并且没有特殊说明时,说的就是补码。
先看补码是如何将之前的无符号数变成可以表示负数的整数的。它把本来的\([0, 2^w-1]\)之间的\(2^w\)个数平均分成了\(2^{w-1}\)个负数和\(2^{w-1}\)个非负数。但由于零是非负数,所以能表示的正整数自然比负整数少了一个。
最终,\(w\)位补码数能表示的整数范围为\([-2^{w-1}, -2^{w-1} -1]\)。
上面只是介绍了补码数最终所达到的效果和能够表示的整数范围,但实际上如何表示的还没说。补码数采取的办法很简单,就是将\(w\)位之中的最高位加上了一个\(-1\)的权重。于是我们可以这样定义函数\(B2T\):
考虑4位补码数[1000]
,它是四位补码数中最小的一个,因为除了一个负权之外,什么都没有,它的值是\(-2^3=-8\)。考虑[0111]
,它是四位补码数中最大的一个,因为它没有负权,完全由\(w-1\)位的纯正数相加而成,它的值为\(-2^{w-1} - 1 = 7\)
无符号有符号转换
规则就是位值不变,只变解释方式。
同样的二进制表示[1000]
,使用无符号解释方式就是8,使用有符号解释方式就是-8。
所以我们可以定义无符号数U转有符号数T的函数\(U2T_w(\vec{x})=B2T_w(U2B_w(x))\),即先将对应的无符号数转成位表示,再用有符号的方式解释它。有符号转无符号函数\(T2U_w(\vec{x})=B2U_w(T2B_w(x))\)
上面的转换公式虽然正确但却有点麻烦,无论如何都需要将一种表示转换成位表示,再将位表示转换成另一种表示。实际上,相同的位,用无符号数和有符号数来解释唯一不同的地方就是最高位究竟是否具有负权,剩下都相同,所以我们可以导出:
也就是说,在同样的位表示下,你用无符号和有符号数的方式来解释,它们中间差了\(x_{w-1}2^w\),所以:
整数扩展
整数扩展即把一个\(w\)位的整数换用\(w'\)位来表示,\(w<w'\)。在C语言中你可以认为是short
向int
转换。
无符号数扩展
只需要在前面补零即可
有符号数扩展
使用补码的有符号数扩展时,假设原来是\(w\)位,\(\vec{x}=[x_{w-1}, x_{w-2}, ..., x_0]\),扩大\(k\)位时,可以这样表示:\(\vec{x'}=[x_{w-1}, x_{w-1}, x_{w-1}, ..., x_{w-2}, ..., x_0]\)。即把前面填满\(x_{w-1}\),填\(k\)次。
我们需要证明\(B2T_{w+k}(\vec{x'}) = B2T_{w}(\vec{x})\),其实只需要证明扩展一位时补码数值不会发生变化即可证明\(k\)为任意正整数时值都不会发生变化。
整数截断
整数截断即把一个\(w\)位的整数换用\(w'\)位来表示,\(w>w'\)。在C语言中你可以认为是int
向short
转换。
无论是有符号还是无符号,截断操作都是直接砍掉多余部分。
无符号整数截断
如果你想把一个无符号数\(x\)缩小成\(k\)位,那么得到的结果\(x'=x \mod 2^k\)。因为截断操作是直接砍掉多余的高位,除了剩下的\(0\)到\(2^{k-1}\)位之外,剩下那些高位去余\(2^k\)得到的都是0,所以\(B2U_k(\vec{x})=B2U_w(\vec{x})\mod 2^k\)
有符号数截断
基于补码的有符号数\(x\)进行截断时也是直接截掉高位,所以和无符号一样,所以\(x'=U2T_k(x\mod 2^k)\)(这里假设x是无符号整数了)
整数加法
两个整数相加,主要看如果结果超出了当前位数所能表示的范围时如何处理,这种情况称作溢出。
溢出的处理很简单,就是截断多余的位,下面主要研究截断之后得出的结果与数学中的加法运算结果的差异。
下面,符号\(x+^u_w\)代表无符号加法,符号\(x+^t_w\)代表有符号加法,符号\(+\)代表数学中的加法
无符号整数加法
无符号数的溢出处理很简单,因为两数相加,由于进位,最多可能导致和比两个数的位数多1位,所以当发现溢出时,直接把由于溢出而产生的最高位贡献的值\(2^w\)给去掉就好了。
由于只是简单的截断,无符号数加法也相当于\(x+^u_w y = x + y \mod 2^w\)。
溢出会导致结果小于任何一个数
有符号整数加法
基于补码的有符号的稍微复杂点,因为它要处理两个边界,两个负数相加可能导致向负边界溢出,两个正数相加有可能导致向正边界溢出,而截断后与预期结果的差异在正溢出和负溢出时是不同的。
下面是公式T.a
正溢出,假设\(w=4,x=[0110]_2=6,y=[0101]_2=5\),数学上,\(x+y=11\),发生了溢出,位表示上是\([1011]\),这里虽然不用截断,但如果按照补码数来解释这个位表示,结果则是\(-8+2+1=-5\),与之前的\(11\)相比,正好相当于减去\(2^w=16\)。
再举个负边界溢出的例子,假设\(w=4,x=[1010]_2=-6,y=[1011]=-5\),此时数学上\(x+y=-11\),位表示上是\([10101]\),但需要截断最高位,所以相当于最终结果是\([0101]=5\),也就是相当于将最高位的负权去掉了,也就是加上了\(2^w\)。
下面进行一些小推导,由于在整个运算过程中,位表示也是不变的,截断也是不变的,变得还只是运算,截断后解释这些位的方式,所以
利用一些之前的理论对这个进行化简,消除这些位转换,下面是公式T.b
- 转换成无符号加法
- 使用\(T2U_w(x)=x + x_{w-1}2^w\)这个公式
- 转换成普通加法,注意转换成普通加法需要一个\(mod 2^w\)
- 将具有\(2^w\)系数的项去除得到最终表达式
通过逻辑推导从理性上理解后,我们还需要感性的理解来方便记忆这个公式,查看公式
T.a
,无论是对于正溢出还是负溢出还是正常运算,我们都要直接把x加上y,然后如果产生了位扩展(这时是负溢出)就通过mod
运算截断,保证始终是w位,如果没产生,那么mod
操作也相当于没做,然后再用有符号的方式来解释这个数。
负溢出会导致相加后结果大于任何一个数,正溢出会导致结果小于任何一个数
整数乘法
不管是无符号还是有符号,两个\(w\)位整数相乘所得到的范围可能需要最多由\(2w\)个位来表示,所以需要截断
无符号整数乘法
有符号整数乘法
乘以常数
因为整数乘法的指令在大多数CPU上要比加法、减法、位运算和移位慢,所以当你编写乘以常数的代码时,编译器经常会将其优化为一些移位操作。
如果要乘以的数是2的幂,那么其效果相当于左移幂次位。而对于任意数,比如x*14
,14可能被分解为2^3+2^2+2^1
,所以,乘法操作可能会被重写成(x<<3) + (x<<2) + (x<<1)
,也可能被分解成2^4-2^1
。
除以常数
除法不用考虑溢出问题,所以这里我们只考虑除以常数时的一些优化手段。通常编译器也会将除法编译成移位操作和加减操作的结合。
除法会引入一个新的问题,就是可能会出现小数,需要按照一定规则进行取整,对于无符号数,除法会向下取整,对于有符号数,取整会向0的一端靠拢,即正数向下取,负数向上取。
无符号除以2的幂
对于无符号除法,由于不会产生负数,所以只需要逻辑右移即可。逻辑右移不会处理符号的问题。而且当你对无符号数进行逻辑右移时,确实会向下取整。
有符号除以2的幂
对于有符号除法,首先需要先考虑符号问题,所以需要使用算数右移,算数右移会复制原始的\(x_{w-1}\)位作为右移最高端产生的新位。
如果你把一个\(w\)位的向量进行逻辑右移3次,那么得到的就是:
然后第二个要考虑的问题就是,如果直接右移只会向下取整,当遇到负号时,比如\(-5.2\),会取整成\(-6\),这不符合有符号整数除法的定义,有符号整数除法会向0取整。
考虑整数\(x,y(y>0)\),\(\lceil x/y\rceil = \lfloor (x+y-1)/y \rfloor\),移位操作\(x<<k\)中\(x\)相当于被除数,\(2^{k}\)相当于除数。所以当除法的结果是负数时,我们只需要在移位之前对对\(x\)加上一个\(2^k-1\)的偏置即可让其向上取整。