二進制補碼

轉載地址:整數二進制補碼的數學原理(two's complement)

最近重新學習CPU體系結構,對使用二進制補碼原理來消除帶符號數和無符號數計算差異,以及整合減法運算器到加法運算器,從而簡化CPU硬件設計的原理很感興趣,所以特地思考了下,查看了一些網上關於two's complement的文章,但大部分還是太過學術,經過整理,我想以一種比較簡潔的方式表達出來。爲了簡單起見,我使用了4位字長的寄存器作爲例子,32位和64位道理一樣。想了解補碼更爲科學的數學原理可以參考wikipedia關於one's complement、two's complement的相關文章。

硬件設計以簡潔爲目標,所以整數的運算最好只有加法,而且不用對符號位進行特殊處理,能達到這個目標嗎?當然可以,那就是使用補碼(two's complement),所謂補碼其實是針對帶符號數來說的,其意思就是正數使用原碼,而負數使用2的字長的指數減負數的絕對值表示,即x = pow(2, word_length) - abs(x),這個補碼的簡單計算方法就是我們計算機書中常說的,將x絕對值取反加1。現在你知道補碼的真正計算方法了吧。爲什麼要將負數表示成這樣呢?這是有數學原理的,這正是本文需要闡釋的內容,充分了解後對CPU常用指令編程就打下了堅實的基礎(general purpose instructions都是針對整數的),以後可能還會增加關於浮點數計算規範的文章。

現在我們來看一個減法:
7 - 6     (式1)
能把它變成一個加法嗎?我來試試:
7 + (16 - 6)- 16    (式2)
16是4位寄存器最小的溢出數(2**4 **表示pow運算),以上兩個式子是完全等價的,在我們看來比較繁瑣的第二個式子卻正是CPU內部整數計算單元所採用的方法,由於一些特殊原因,CPU只需要計算第一個加法,其餘兩個減法分別由編譯器、人或寄存器自動截斷完成了。
經過前面的敘述,我們知道了16 - 6就是-6在4位字長機器上的補碼,這步計算一般是編譯器完成的,將負數直接存儲爲補碼形式,這裏是1010。我們來看看CPU如何計算:
  0 1 1 1        (7)
+ 1 0 1 0     (10)
-----------   ----------
1 0 0 0 1     (17)

以上式子完成了式(2)中前兩步計算,還需要減16才能得到正確結果,神奇的地方到了,因爲機器是4位字長,所以第五位1直接丟棄掉了,就是溢出,這相當於自動減了16,所以最後結果就是0001,等於1,完成了式2個所有計算,得到了正確結果,現在你應該明白了爲什麼會選擇最小溢出數所爲補碼轉換中的被減數了吧,就是爲了完成自動溢出,從而實現最後的減法。

再來看看2個負數相加,看看CPU是如何把它們當純粹二進制運算而結果卻絲毫不差的:
(-6) + (-7)  (式3)
依據上面的規律換成如下式子
[(16 - 6) - 16] + [(16 - 7) - 16]    (式4)
其中(16 - 6)和(16 - 7)部分已經由編譯器完成,就是對應負數的補碼,讓我們來看看CPU的計算內容:
  1 0 1 0     (10)
+ 1 0 0 1      (9)
-----------   ------
1 0 0 1 1      (19)

式4中還需要減2個16,這裏第5位已經自動溢出減了一個16,我們還要減一個16才能得到正確結果,可是寄存器中結果0011,光憑這個結果,我不知道這到底是最終值還是還需要減16,這可太糟糕了,產生這個問題的原因是如果使用全部4位寄存器存儲值時,會產生帶符號數二進制歧義問題,打個比方,-9用二進制補碼錶示是(16 - 9),二進制爲0111,居然和整數的7是一樣的,光憑這串二進制我無法知道它是-9還是7,好吧,我確實聰明,想到了一個辦法,嘿嘿,讓我們來看看4位寄存器能存儲的二進制有哪些:
0 0 0 0
0 0 0 1
0 0 1 0
0 0 1 1
0 1 0 0
0 1 0 1
0 1 1 0
0 1 1 1
1 0 0 0
1 0 0 1
1 0 1 0
1 0 1 1
1 1 0 0
1 1 0 1
1 1 1 0
1 1 1 1
我可以將最高位數字當作解釋數字符號的標誌,如果是0我就當正數解釋,如果是1我就當負數解釋,當正數解釋後不用再減16,直接就是最終結果,而如果是負數則還需要減個16纔是最終結果,因爲我們是用16-x來表示-x的,正好正數負數對半(假設0是正數),再回到上面那個問題,(-6) + (-7)CPU寄存器中最終爲0011,我當應該當正數解釋,正數不用減16,所以最後等於3,不對!應該是-13,還需要減個16纔對,可我們剛說了正數不用減。到底哪裏出問題了?大家思考下。
原來如果我們將二進制用以上闡述的方式解釋,決定了4位二進制表示的數的範圍只能是[-8~7],實際上寄存器如果從左端溢出的話,其值是在[...0\1\2\3\4\5\6\7\-8\-7\-6\-5\-4\-3\-2\-1\0...]不斷循環的,也就是說剛纔的-13從-8向左數5位,又循環回到了3,我們必須有辦法判斷溢出情況,如果我們把寄存器中的二進制當無符號數解釋,那很簡單,只要最高爲產生進位那就溢出了,可如果當帶符號數解釋,如何看最後的值是否溢出呢?
這是個補碼數學原理的精髓所在,有了這個推理,CPU才能做到同等處理帶符號數和無符號數,我們來仔細分析下數學上的原理,在CPU看來寄存器中的就是純粹的二進制,相當於無符號數,如果兩個數相加時,最高位產生了進位,則表示結果肯定位於[16~30],如果次高位向最高位產生了進位則表示結果低3位相加結果範圍位於[8~14],由於最高位溢出被丟棄,表示對最終結果減了16,而次高位向最高位產生進位,表示最終結果最小爲8,現在就有如下幾種結論:
(1)最高位有進位,次高位有進位,則最終結果位於[-8~6]
(2)最高位有進位,次高位無進位,則最終結果位於[-16~-9]
(3)最高位無進位,次高位有進位,則最終結果位於[8~14]
(4)最高位無進位,次高位無進位,則最終結果位於[0~7]
而我們帶符號的解釋方法,決定了數的範圍爲[-8~7],怎麼樣一眼就看出該如何判斷帶符號數計算是否溢出了吧!

然我們來看看CPU EFLAGS寄存器中最常用的6個標誌位CF,PF,AF,ZF,SF,OF,我只解釋CF和OF,其餘的4個都很好理解,CF表示兩個操作數進行二進制整數計算時最高位發生了進位,很顯然可以用來判斷無符號數是否溢出,而OF是寄存器次高位是否向最高位發生進位的標誌(進位1,否則爲0)與CF位的XOR值,是不是很神奇,就是我們最後闡述的四項規則,正好用來判斷帶符號整數是否溢出。

是不是無法想象在一般書上一筆帶過的整數計算用的補碼規範後面卻隱藏了這麼多原理,正是這些特性,決定了處理器設計時採用二進制補碼進行整數計算,他使帶符號數和無符號數的加減運算全部用無符號數加法運算實現,使電路實現大爲簡化,增加了處理器效率,減少了設計製造成本。但整數的乘法/除法運算卻無法這樣處理,這就是爲什麼有帶符號的乘除指令而加法和減法卻沒有,從一定意義上講,其實減法指令只是加法指令的一個包裝,因爲CPU內部沒有減法邏輯,只有加法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章