深刻理解計算機系統:信息的處理和表示(二)整數四則運算

參考自:http://csapp.cs.cmu.edu/express

 

開篇說明一下,本文不是介紹四則運算的具體執行過程,想了解具體過程的孩子們本身去看看計算機組成。app

好了,話很少說。ide

1.  加減法

加法和減法沒有區別,如下內容專一於加法。函數

1.1  無符號數加法

無符號數加法會出現溢出問題,當發生溢出的時候直接扔掉溢出進位。好比1111 + 0001 = 10000 直接扔掉進位1,結果爲0000。考慮到溢出以後,咱們能夠總結出這樣的計算式:優化

image

image

那麼咱們怎麼來判斷溢出呢?ui

對於s= x+ y; 當s <x 或者 s < y的時候就是溢出了。緣由在於溢出的時候s=x+y-2w。而x,y都是小於2w的因此s<x ,s<y。this

因而,咱們能夠寫出函數來提早判斷兩個無符號數想加是否會溢出:lua

/* Determine whether arguments can be added without overflow */
int uadd_ok(unsigned x, unsigned y){
    unsigned  temp= x+y;
    return temp>=x;
}

下面給出一張圖,全面展現四位無符號二進制樹之間的想加狀況:spa

image

好了,以前提到加法和減法沒什麼區別,那麼咱們怎麼算無符號數減法呢?要知道無符號數但是沒有「相反數」這個東西的。code

爲了解決這個問題,咱們引入一個 加法逆元(additive inverse) 的概念。假設x的加法逆元是y,當且僅當(x+y)%2w=0,其中x,y爲w位的二進制數。

加法逆元的計算公式:

image

之後計算減法的時候,就將被減數轉換爲其加法逆元,這樣再相加就行了。

 

1.2  有符號數加法

有符號加法和無符號加法產生的二進制是同樣的,它也會產生溢出,因爲符號的緣由它有兩種溢出 正溢出和負溢出。正溢出是指兩個兩個正數想家結果爲負數,好比0111 + 0001 =1000。負溢出就是兩個負數相加爲正數1001 + 1001 =0010。計算公式以下:

image

image

給出四位二進制有符號數相加的所有狀況圖:

image那麼怎麼判斷是否溢出呢?

原理就是 負負得正,正正得負 就是溢出啦

/* Determine whether arguments can be added without overflow */
int tadd_ok(int x, int y) {
    int sum = x+y;    
    int neg_over = x < 0 && y < 0 && sum >= 0;
    int pos_over = x >= 0 && y >= 0 && sum < 0;
    return !neg_over && !pos_over;
}

固然了,若是有人神經叨叨的給你看他的實現方法:

/* Determine whether arguments can be added without overflow */
/* WARNING: This code is buggy. */
int tadd_ok(int x, int y) {
    int sum = x+y;
    return (sum-x == y) && (sum-y == x);
}

這段代碼毫無疑問是錯誤的,爲何呢?由於咱們知道加法是符合交換律的,那麼(x+y)-x = (x-x)+y,也就是說上述判斷條件始終成立。不信你能夠驗證。

說完加法,咱們來看看怎麼將減法轉換爲加法。固然是取相反數了,關鍵是,怎麼取相反數?這是一個頗有學問的地方,技巧妙的一談糊塗。

x相反數是y,當且僅當x+y=0。那麼咱們顯然獲得y=-x這個公式了,可是要是有人問你最小負數的相反數是多少,你怎麼辦?舉個例子,對於8位二進制而言,最小負數-128,-(-128)=128>127(8位二進制最大正數)。

哈哈,怎麼辦,怎麼辦。

結論是最小負數的相反數就是它本身!!你能夠算一算1000 0000 + 1000 0000 =0000 0000 符合定義!

因而就有咱們的求負數的公式了:

image

哈哈,看完這個咱們再來看看怎麼驗證兩個數相減會不會溢出,先看看下述代碼:

/* Determine whether arguments can be subtracted without overflow */
/* WARNING: This code is buggy. */
int tsub_ok(int x, int y) {
    return tadd_ok(x, -y);
}

這個對不對呢?

應該是不對的,由於Tmin的相反數仍是他本身,當x爲正數,y爲Tmin的時候,這個函數是會報錯的,可是其實是不會溢出的。

下面接着來講怎麼實際操做來取相反數:一種方法是 各位取反後加一

image

第二種方法:咱們仔細看看這個咱們能看出一個規律,就是說咱們沿着二進制從右往左走,知道遇到第一個1,而後將第一個1以後的每一個位取反就行了。

2.  乘法

關於具體怎麼作乘法本身去查閱計算機組成原理課本。咱們先來看具體的二進制乘法的例子:

image

咱們發現一個現象,就是說有符號二進制乘法和無符號二進制乘法,他們得到的最終結果的二進制表示是同樣的!!、

好吧,處於嚴謹考慮,咱們來證實一下下:

image

那麼咱們怎麼來判斷乘法會不會發生溢出呢?咱們注意到,上面那個圖中的乘法都是溢出的。

先給出代碼,你們能夠看看這個代碼對不對?

/* Determine whether arguments can be multiplied without overflow */
int tmult_ok(int x, int y) {
    int p = x*y;
    /* Either x is zero, or dividing p by x gives y */
    return !x || p/x == y;
}

好吧,我認可我頑皮了,由於大部分同窗(包括曾經的我)根本看不懂這個是什麼東東!腦子裏就倆個字:尼瑪!

咱們從理論上證實一下下:

首先咱們知道對於w位的x和y,x*y是能夠用2w位二進制來表示的,咱們將這2w位二進制拆爲兩部分,高w位(用v來表示)和低w位(用u來表示),因而咱們獲得x*y=v*2w + u. u表明了代碼中p的二進制表示,根據無符號數轉換爲有符號數的公式咱們獲得:u = p + p w−1 2w,因而咱們能夠將x * y = p + t * 2w. 根據溢出的定義,該乘法發生溢出當且僅當 t != 0。

其次,當x!=0的時候咱們能夠將p表示爲p = x * q + r, |r| < |x|。因而咱們有x * y = x * q + r + t * 2w。咱們須要推導出t == 0的等價條件。當t = 0的時候,咱們有x * y = x * q + r,而|r| < |x|,因此必有q=y;反過來,q=y的時候咱們也能夠推出t=0。也便是說t=0這個條件等價於q=y這個條件(前提是x不爲0)。

q=p/x;

因而咱們就獲得了咱們代碼裏面的判斷條件: !x || p/x == y

那麼若是咱們不容許使用除法,可是容許你使用long long這個數據類型,你怎麼來作乘法的溢出檢查呢?

哈哈,其實這個就簡單了。

/* Determine whether arguments can be multiplied without overflow */
 int tmult_ok(int x, int y) {
     /* Compute product without overflow */
     long long pll = (long long) x*y;
     /* See if casting to int preserves value */
     return pll == (int) pll;
 }

 

哈哈,說完這些以後咱們來看看怎麼將乘法轉換爲 移位和加法操做, 由於這兩種操做時很是快速的。

咱們來看,怎麼將經過移位的方法一個數擴大兩倍?很顯然是左移一位。擴大四倍呢?左移兩位。

其實咱們就類比十進制乘法  列豎的計算式子就能夠了。

這裏面有個能夠被優化的技巧,咱們來看。加入被乘數是[(0 . . . 0)(1 . . . 1)(0 . . . 0) ].那麼咱們還要一個個的移位相加麼?其中連續的1,起於從右到左的第m位,終於第n位。

咱們能夠這樣,(x<<n+1) - (x<<m)。

好了,乘法也就這麼多了,下面是除法。

3.  除法

除法這一塊咱們只涉及除以一個2k次方。對於正數以及無符號數而言,這意味着右移k位,高位補0。下圖中斜體的0都是後來補的0。

image

而對於負數而言,咱們須要採用邏輯右移,也就是說高位補1。例子見下圖:

image

可是咱們在圖中發現一個問題,就是結果可能跟真實的值相差一,好比-12340/8應該爲-48而不是-49,-7/2=-3而不是-4。怎麼處理這個問題呢?

咱們能夠對負數作一點預處理,使得x=x+y-1。其中y就是咱們的除數。

咱們先來看看這麼作對不對,(-7+2-1)/2 = (-6/2) =-3 沒有問題啊。

證實以下:假設x = qy + r,其中0 ≤r <y。那麼,(x + y − 1)/y = q + (r + y − 1)/y。當r=0的時候x/y=q,不然x/y=q+1。符合要求!!

代碼實現是這樣的:(x<0 ? x+(1<<k)-1 : x) >> k

哈哈,妙吧!

那麼若是僅僅容許使用移位和&操做以及加法,怎麼代碼實現除法呢?(注意除數爲2k)

int div16(int x) {
/* Compute bias to be either 0 (x >= 0) or 15 (x < 0) */
    int bias = (x >> 31) & 0xF;
    return (x + bias) >> 4;
}

看看這個代碼寫的!!亮瞎了吧。

當x爲正數的時候,bias=0, 這沒錯。

當x爲負數的時候,bias爲15=16-1。又沒問題!

 

好了,除法也就這些了!

4  總結練習

Assume we are running code on a 32-bit machine using two’s-complement arithmetic
for signed values. Right shifts are performed arithmetically for signed values
and logically for unsigned values. The variables are declared and initialized as
follows:
int x = foo(); /* Arbitrary value */
int y = bar(); /* Arbitrary value */
unsigned ux = x;
unsigned uy = y;
For each of the following C expressions, either (1) argue that it is true (evaluates
to 1) for all values of x and y, or (2) give values of x and y for which it is false
(evaluates to 0):
A. (x > 0) || (x-1 < 0)
B. (x & 7) != 7 || (x<<29 < 0)
C. (x * x) >= 0
D. x < 0 || -x <= 0
E. x > 0 || -x >= 0
F. x+y == uy+ux
G. x*~y + uy*ux == –x

 

解答以下:

A. (x > 0) || (x-1 < 0)
False. Let x be −2,147,483,648 (TMin32). We will then have x-1 equal to
2147483647 (TMax32).


B. (x & 7) != 7 || (x<<29 < 0)
True. If (x & 7) != 7 evaluates to 0, then we must have bit x2 equal to 1.
When shifted left by 29, this will become the sign bit.


C. (x * x) >= 0
False. When x is 65,535 (0xFFFF), x*x is −131,071 (0xFFFE0001).


D. x < 0 || -x <= 0
True. If x is nonnegative, then -x is nonpositive.


E. x > 0 || -x >= 0
False. Let x be −2,147,483,648 (TMin32). Then both x and -x are negative.

 

F. x+y == uy+ux
True. Two’s-complement and unsigned addition have the same bit-level behavior,
and they are commutative.

G. x*~y + uy*ux == -x True. ~y equals -y-1. uy*ux equals x*y. Thus, the left hand side is equivalent to x*-y-x+x*y.

相關文章
相關標籤/搜索