這幾天一直在寫《深入理解計算機系統》第二版中第二章的家庭作業,費了幾天的時間,終於完成了。當初碰到若干題不會,在網上也沒有搜索到答案。現在,我把這份自己完成的答案分享上來,與大家交流思想。其中錯誤一定會存在,如果有錯誤,希望指出來,共同進步。
2.67
A:左移位數大於等於int長度。
B:可以考慮用兩次左移來實現<<32:
int set_msb=1<<31;
int beyond_msb=set_msb<<1;
C:可以考慮用三次左移來實現<<31與<<32:
int temp=1<<15;
temp<<=15;
int set_msb=temp<<1;
int beyond_msb=temp<<2;
2.68
Q:Clear all but least significant n bits of x,即保留x的低n位,1<=n<=w
A:可以參考2.66的提示。生成低n位全為1,其余位全為0的掩碼,通常想到的是
(1<<n)-1
但是n==w的時候就會出錯,左移的位數等於int長度(見2.67A),這時我們可以采用兩次左移來實現:先生成低n-1位全為1,其余位全為0的掩碼m,然后讓m左移1位,然后加1即可,手工檢驗n==1時此情形仍滿足,故最后的掩碼為{[1<<(n-1)-1]<<1+1},此掩碼與x相與即可:
int lower_bits(int x, int n)
{
return ((1<<(n-1)-1)<<1+1)&x;
}
2.69
Q::Do rotating right shift. Assume 0<=n<w
A:實際上相當於將x循環右移n位。我們先取x的低n位,要特別注意n==0的情形,參照2.68,使用((1<<n)-1)&x來取得x的低n位,之所以不采用2.68最終答案所示的方法的原因是n可以取到0,但是取不到w,而2.68中,n取不到0,而能取到w。
然后我們將x的高(w-n)位右移n位,在題目假設(P154頁)中,右移操作是算數右移,高n位擴展了x的符號位,而我們需要的是邏輯右移。邏輯右移的解法如2.63中所示:
unsigned srl(unsigned x, int k)
{
unsigned xsra = (int) x>>k;
return ((1<<(w-k-1)-1)<<1+1)^xsra;
}
同時需要注意n為0的情形,處理該情形的方法如2.68所述。將x的低n位左移w-n位,異或上slr(x,n)即可:
((((1<<n)-1)&x)<<(w-n-1)<<1)^slr(x,n)
在上式中,同樣要注意k=0的情形。
2.70
Q:Return 1 when x can be represented as an n-bit, 2’s complement number; 0 otherwise. 1<=n<=w.
A:對於w位的二進制補碼數x,它能不能表示成n位的二進制補碼數需要看它的最高w-n+1位是不是全0(正數)或者全1(負數)。這是由於:
我們考慮一個n-bit的二進制補碼數a,最高位(第n-1位)是符號位。對於正數,有a[n-1]=0,將其擴展為w-bit時,第n-1位及其左邊的w-n位都為0。這個條件是充分必要的,我們可以反證出,如果前面最高的w-n+1位中(除了最高位),某一位有1,則一定不能用n-bit的二進制補碼數表示出。
而對於負數,有a[n-1]=1,將其擴展為w-bit時,第n-1位及其左邊的w-n位都為1。這個條件同樣是充分必要的,在此不再進行說明。
由此我們可以得到解法:(1)用掩碼屏蔽掉x的高w-n位,即取得00…x[n-1]x[n-2]…x[0]。(2)然后將x[n-1]擴展到最高的w-n位中,得到x’=x[n-1]x[n-1]…x[n-1]x[n-2]…x[0]。(3)最后return x’==x即可。
步驟:
(1)y= (~((~0)<<n-1<<1))&x(分兩次左移,為了處理n==w的情形)
(2)z=y>>n-1,z[0]是符號位,z==1為負,z==0為正就是符號位。
x’=((0-z)&(~0<<n-1<<1))|y
(3)return x’==x
2.71
A:packed_t是無符號數,而它包裝的4個字節都是有符號數,1byte的包裝在無符號數中的有符號數擴展后符號位並沒有擴展。例如,1byte的0xFF表示-1,而擴展成32位后應該為0xFFFFFFFF。而按照原函數中所示,擴展后結果是0x000000FF。
B:其實這個題可以將unsigned轉換成signed,然后利用算數右移來取得高位符號位的擴展。但是我們有另外一個解法:
unsigned x=word>>(bytenum<<3);
x<<=24;
x>>=24;
unsigned y=x>>7;
y<<=8;
return x-y;
這個解法不理解的話,自己動手找個示例畫一畫就理解其中的原理了。
2.72
A:sizeof返回size_t,實際上是無符號整型,而maxbytes-sizeof(val)則會隱式裝換為無符號數減法,所的結果始終是正數。測試條件>=0始終成立。
B:maxbytes>=0&&maxbytes>=sizeof(val)
2.73
編寫一個函數,當正溢出時,將結果設為Tmax,當負溢出時,將結果設置為Tmin。
首先,我們需要判斷是否發生了溢出:x、y同號,而與x+y異號時則發生了溢出:即(1)=((x^(x+y))&(y^(x+y))>>(w-1)。經過這個式子處理,發生溢出時,(1)式結果為1111….111,未發生溢出時,(1)式結果為0000….000。
然后,我們判斷發生的溢出是正溢出還是負溢出:即
(2)=(1)&(x>>(w-1)),正溢出時,(2)為0000…000;負溢出時,(2)為11111…111。
未發生溢出時,結果為x+y,而發生溢出時,(x+y)這部分結果需要屏蔽掉,代之以相應的Tmax或者Tmin。故最終的結果包含了(x+y)|(1)。當x+y沒有發生溢出時,這部分結果為x+y,而發生溢出時,這部分結果為1111…111。而當正溢出時,我們需要的結果是0111…111,負溢出時,我們需要的結果是1000…000,即我們需要在(x+y)|(1)的基礎上減去相應的值:
當未發生溢出時,減去0
當正溢出時,減去1000…000
當負溢出時,減去0111…111
綜上所述,我們可以得到需要減去的那部分的值的表達式:
(1)&((1<<(w-1))^(2))。故最終的結果為:
(x+y)|(1)- (1)&((1<<(w-1))^(2)),其中(1)、(2)的表達式如上述分析中所示。
2.74
這道題是測試減法是否發生了溢出。如果我們一下子不好考慮的話,先分析在減法中哪些情況會發生溢出。設x-y=z,
當x>0,y<0,z<=0時會發生溢出;
當x<0,y>0,z>=0時會發生溢出。
即x、y異號,並且x與z異號時會發生溢出。特別注意z=0的情形。
最后的答案可以寫為:
return (x^y!=0)&&(z==0||x^z!=0)
2.75
設兩個bit串的signed表示為x’、y’,unsigned表示為x,y。根據本章所學,有x’=x+x[w-1]*2w,y’=y+y[w-1]*2w。
x’*y’ = (( x+x[w-1]*2w) * (y+y[w-1]*2w) mod 2w
=(x*y+(y*x[w-1]+x*y[w-1])*2w+x[w-1]*y[w-1]*2w) mod 2w
注意上式,(x*y)的高w位在(x*y)中計算出,x’*y’相對於x*y高w位所多出的數是y*x[w-1]+x*y[w-1]。故經過unsigned_high_prod計算出的結果,需要加上x(當y為負時),與y(當x為負時),就是signed_high_prod的結果。
2.76
A:a<<2+a
B:a<<3+a
C:a<<5-a<<1
D:a<<3-a<<6
2.77
書中P130頁有原理敘述。對於本題,我們首先需要判斷x是正數還是負數:
int a=x>>w-1;
a==111….111則為負數,a==000….000則為正數。正數時不用額外考慮,而負數時需要加上一個bias偏差來向0舍入:
int result=((y-1)&a+x)/y,y=1<<k,帶入之后即可得到最終答案。
2.78
5*x/8:
x<<2+x得到5*x,而除以8的操作需要利用2.77題的結果,最終結果是: return divide_power(x<<2+x,3)。
2.79
這道題與2.78的唯一區別是計算5x/8時不能發生溢出。因為5x/8=x/2+x/8。當x為負數時,我們需要求得5x/8向上舍入的結果,在博客上我們使用celling(x)來表示x向上舍入的正數。我們可以通過對幾個x的取值進行驗證,得到如下關系:
celling(5x/8)=celling(x/2)+celling(x/8)+1,當x=-5-8k或-7-8k,k=0,1,2…
celling(5x/8)=celling(x/2)+celling(x/8),其他情況。
由2.77可以得到:
celling(x/2)=divide_power2(x,1)
celling(x/8)=divide_power2(x,3)
下面判斷x=-5-8k或者x=-7-8k。負值不好判斷,而正值比較好判斷,即:-x=5+8k,后3位為101。而-x=-7+8k,后三位為111。
故我們可以得到,
celling(5x/8)= celling(x/2)+celling(x/8)+(-x&0x5||-x&0x7)&0x1。
而當x為正數時,我們也需要經過相似的判斷,經過計算,可以知道當x=5+8k或x=7+8k時,同樣需要多加一個1。
故而,最后總的式子為:
celling(5x/8)= celling(x/2)+celling(x/8)+(-x&0x5||-x&0x7||x&0x5||x&0x7)&0x1
2.80
A:~((1<<n)-1),注意此時0<n<w。
B:(1<<(n+m)-1)-(1<<m-1)=1<<(n+m)-1<<m。
2.81
A:錯誤,考慮x=0,y=1<<31,x>y,而-x>-y。
B:正確。數學中常用的分配律、結合律即可證明,<<5即乘以32。
C:錯誤。~x+~y=(-x-1)+(-y-1)=-(x+y)-2,不等於-(x+y)-1
D:正確的。signed與unsigned的加法計算在本質上是一致的。
E:正確。右移會向小值舍入。
2.82
A:我們要求出那個無窮循環的串的數學公式表示。令其為V,則有
V<<k=V+Y(高等數學知識)
則V=Y/(2k-1)
B:(a) 1/7 (b) 0.6 (c) 1/9
2.83
浮點數最高位為符號位。先處理-0、+0比較的情況:
ux<<1==0&&uy<<1==0
x為正,y為正:
sx==0&&sy==0&&ux>=uy
x為負,y為負:
sx==1&&sy==1&&ux<=uy
x為正,y為負:
!sx&&sy
以上式子使用||連接起來就是最終答案。
2.84
A:f=0.01,M=1.01,E=2。注意到E=2而原始的exp-bias=E,exp=1+2k-1。
B:f=0.11…1,M=1.11…1。而注意要精確表示最大的奇數。f的最后一個1一定要保留,所以E最大為n。此時,原始的exp=n+2k-1-1。
C:最小的正normalized值的表示為
2^(2-2k-1)。要求它的倒數,則指數部分應該是2^(2k-1-2),以使得乘積為1。而數值部分,則為0。
2.91
unsigned sign=f>>31;
unsigned exp=f>>23&0xFF;
unsigned frac=f&0x7FFFFF;
if(!(exp^0xFF==0&&frac!=0)) f|=1<<31;
return f;
2.92
unsigned sign=f>>31;
unsigned exp=f>>23&0xFF;
unsigned frac=f&0x7FFFFF;
if(!(exp^0xFF==0&&frac!=0)) f+=1<<31;
return f;
2.93
unsigned sign=f>>31;
unsigned exp=f>>23&0xFF;
unsigned frac=f&0x7FFFFF;
if(exp^0xFF==0&&frac!=0) return f;
else if(exp>0) exp=exp-1;
else {
//round to even
if(frac&0x1!=0&&(frac>>1)&0x1==1)
frac=frac>>1+1;
else frac=frac>>1;
}
return (sign<<31)|(exp<<23)|frac;
2.94
unsigned sign=f>>31;
unsigned exp=f>>23&0xFF;
unsigned frac=f&0x7FFFFF;
if(exp^0xFF==0&&frac!=0) return f;
else if(exp&0xFE!=0&&exp!=0) exp=exp+1;
else if(exp==0)
{
if(frac>>23&0x1==0) frac<<1;
else
{
exp=1;
frac<<1;
}
}
return (sign<<31)|(exp<<23)|frac;
2.95
i是整數,故而E部分的指數最小也為0+127=127。不包括E為0的情況。我們首先判斷最高位的1在哪個位置。
//符號位
int sign=i>>31;
//求i的絕對值
if(!sign) i=~i+1;
//求數值位最高位的1所在位置
int pos=0;
if(i>>16) {pos+=16; i>>=16;}
if(i>>8) {pos+=8; i>>=8;}
if(i>>4) {pos+=4; i>>=4;}
if(i>>2) {pos+=2; i>>=2;}
if(i>>1) {pos+=1; i>>=1;}
exp=pos+127;
//屏蔽掉數值部分為1的最高位(第pos位),這一位在浮點數中是隱式顯示表示的。
i=~(1<<pos)&i;
//第pos位要左移或者右移到第23位,使得低23位是frac部分。
if(i>>23)
{
frac=i>>(pos-23);
//最低(pos-23)位要看是否需要round to even,並且pos!=23
if(!(pos^0x17)&&(((1<<(pos-23)-1)&&i)^(1<<(pos-23-1)))==0))
{
//round to even
if(frac&0x1) frac+=1;
//發生溢出時,需要進位
if(frac==0) exp+=1;
}
}
else
{
frac=i<<(23-pos);
}
return (sign<<31)|(exp<<23)|frac;
2.96
這道題還是比較繁瑣,解法可類比2.95,但不再具體寫代碼了。在求解的時候,首先不考慮符號位,先求絕對值的表示,設這個絕對值為ret。
由於采用了round to zero的策略,故而不需要考慮“入”,只需要考慮“舍”即可。
(1)exp<0+127,ret=0
(1)exp=0+127,ret=1;
(2)exp=255,frac!=0時,ret=0x80000000
(3)exp等於其他值時,我們先把frac部分提出來,在第23位變為1,這是因為在float表示中,這個整數部分的1被隱藏了,我們求解int的時候,需要將其加上。此時,小數點在第23位的右側。
當exp=127時,frac>>=23;
當exp=128時,frac>>=22;
……
當exp=150時,frac>>=0;
當exp=151時,frac<<=1;
當exp=152時,frac<<=2;
……
當exp=157時,frac<<=7;
之后,再左移的話,就會溢出。也即exp>157時,ret=0x80000000。
最后,如果是正數的話,直接拼裝一下。而如果是負數的話,先按正數拼接,然后變反即可。不需要考慮Tmin的原因是float沒有足夠的精度來表示Tmin,故參數給出的f不可能被表示成Tmin。