上下文約束
默認(rèn)圍繞8位計(jì)算機(jī)展開討論。
問題
在進(jìn)入正文之前,先提三個問題:
- 計(jì)算機(jī)中的數(shù)為什么用補(bǔ)碼(2's complement)來表示和存儲?
- 補(bǔ)碼的計(jì)算規(guī)則是怎么來的?
- 計(jì)算機(jī)是如何區(qū)分unsigned int和int?
眾所周知,二進(jìn)制是一種記數(shù)系統(tǒng)(類比十進(jìn)制),而補(bǔ)碼就是該系統(tǒng)之上的編碼協(xié)議。協(xié)議是為了無序信息流變得規(guī)整,讓人能夠控制它。從這方面猜測,補(bǔ)碼產(chǎn)生的原因是為了最小化硬件設(shè)計(jì)的成本,這大概也是最初的軟件定義硬件(SDH)。
當(dāng)我們想象比特流的存儲過程時,不免好奇自己頭腦中的數(shù)值概念(尤其是負(fù)數(shù)和小數(shù))怎么被計(jì)算機(jī)編碼成有意義的比特流?這些比特流如何被正確地計(jì)算成另一種比特流?在更高層次上,編程語言中的short, int, unsigned int, long, long long等數(shù)值類型是怎樣被計(jì)算機(jī)正確地識別的?
通常,協(xié)議是處理復(fù)雜度的好方法,但隱藏在協(xié)議背后的原因比它本身更有探索的價值。如果你也感同身受,那么閱讀下文就是合適的。
為什么用補(bǔ)碼?
概念解釋
原碼[1](True form)
原碼是指一個二進(jìn)制數(shù)左邊加上符號位后所得到的碼,且當(dāng)二進(jìn)制數(shù)大于0時,符號位為0;二進(jìn)制數(shù)小于0時,符號位為1;二進(jìn)制數(shù)等于0時,符號位可以為0(+0)或1(-0)。反碼[2](One's complement)
反碼是帶有符號位的二進(jìn)制數(shù)表示;負(fù)數(shù)的反碼是將其對應(yīng)正數(shù)按位取反,正數(shù)和0的反碼就是該數(shù)字本身。補(bǔ)碼[3](Two's complement)
補(bǔ)碼也是帶有符號位的二進(jìn)制表示;負(fù)數(shù)的補(bǔ)碼是將其對應(yīng)正數(shù)按位取反再加1,正數(shù)和0的補(bǔ)碼就是該數(shù)字本身。
三者的關(guān)系很密切,是1對1的單射關(guān)系。準(zhǔn)確地說,補(bǔ)碼下可表示的最大負(fù)數(shù)沒有對應(yīng)的原碼和反碼。具體原因,待會兒做詳細(xì)討論。
從實(shí)際應(yīng)用的角度提問,現(xiàn)代計(jì)算機(jī)中數(shù)據(jù)的存儲形式是原碼、反碼還是補(bǔ)碼?這個問題其實(shí)可以規(guī)約到另一個問題上:哪種存儲形式最有利于計(jì)算機(jī)進(jìn)行運(yùn)算?從這個角度思考就不難得出答案,當(dāng)然是補(bǔ)碼,不然,要是原碼和反碼都夠用,為啥設(shè)計(jì)出補(bǔ)碼?
那么,為什么原碼和反碼不夠用?單獨(dú)從數(shù)據(jù)表示來看是無法得出結(jié)論的,需要從計(jì)算的角度思考。我們都知道,二進(jìn)制是以2為基數(shù)的記數(shù)系統(tǒng),和十進(jìn)制、六十進(jìn)制的記數(shù)本質(zhì)相同。在最開始,記數(shù)系統(tǒng)中是沒有負(fù)數(shù)的,引入負(fù)數(shù)是為了統(tǒng)一概念相反的事物。比如:收入和支出,支出就可以視為負(fù)的收入。如果用恒等式表達(dá)收支平衡收入 = 支出
,將支出移項(xiàng)到左邊就得變號收入-支出 = 0
,換句話說,收入+(-支出) = 0
,此時表達(dá)了總收入為0這個事實(shí)。相同的概念對于運(yùn)算的意義重大,以前需要設(shè)計(jì)減法的運(yùn)算規(guī)則,現(xiàn)在用加法規(guī)則就可以替代。在人類看來,這意味著概念的統(tǒng)一,而對于計(jì)算機(jī)而言,這簡化了ALU(算術(shù)邏輯單元)中集成電路的設(shè)計(jì)。
既然如此,負(fù)數(shù)在二進(jìn)制中的表示就尤為重要。按照原碼的定義,-1的二進(jìn)制表示是1000 0001
,那么計(jì)算1-1
,也就是計(jì)算0000 0001 + 1000 0001 = 1000 0010
,這個數(shù)表示的是十進(jìn)制中的-2。1-1=-2
當(dāng)然是錯的,它為什么是錯的呢?因?yàn)榉栁灰矃⑴c運(yùn)算了,上面的算式其實(shí)表達(dá)的是1+129=130
這個算式。
看到這里,或許需要思考的是數(shù)的表示和它們之間的運(yùn)算是不對稱的疑問?究其緣由是,數(shù)在計(jì)算機(jī)中表示是人為定義的,我們規(guī)定了左邊的1是符號位,但是ALU不知道,而且也不應(yīng)該知道。為什么呢?因?yàn)槿耸巧谱兊模竺孢€會出現(xiàn)無符號數(shù),那時候左邊的1可就不能視作符號位。
在繼續(xù)探索之前,我們先補(bǔ)充點(diǎn)數(shù)學(xué)知識。
同余
當(dāng)兩個整數(shù)除以同一個正整數(shù),若得相同余數(shù),則這兩個整數(shù)同余[4]。記為:
讀作a與b關(guān)于模m同余。同余的數(shù)具備很多性質(zhì),其中有一條保持基本運(yùn)算:
我們用這個性質(zhì)來反觀一下日常生活中的12小時制計(jì)時法。
0點(diǎn)和12點(diǎn)關(guān)于12點(diǎn)同余,5和5當(dāng)然關(guān)于12點(diǎn)同余,那么有:
所以5和17關(guān)于12同余。當(dāng)我們說下午5點(diǎn)時,其實(shí)也就是在說17點(diǎn)。負(fù)數(shù)也滿足這樣的關(guān)系,即-5和7關(guān)于12同余。
負(fù)數(shù)的反碼
按照定義我們求得-1的反碼,1000 0001
的反碼為1111 1110
即254,-1和254相對于255(1111 1111
)同余。依據(jù)同余的基本性質(zhì),
取c為1,那么
即0和255相對于255同余。當(dāng)然,正確性顯而易見。但是,這里面也出現(xiàn)了一個問題,0和255(-0)都是0這個數(shù)在反碼中的正確表達(dá),這也是負(fù)數(shù)的反碼表示數(shù)的范圍是[-127, 127],總數(shù)是255個的由來,就像12小時制計(jì)時0和12都是零點(diǎn)的表達(dá),那么對于判0運(yùn)算而言,就得有兩手準(zhǔn)備。簡單起見,有沒有其他方式去掉這種特殊情況呢?于是,補(bǔ)碼順勢上位。
負(fù)數(shù)的補(bǔ)碼
由于0和255都是8位計(jì)算機(jī)中0的合法表達(dá),容易想到的一種方法就是去掉一個,而且還得保留同余的性質(zhì)。
假如把相對于255的同余變成256的同余是否有幫助呢?此時,0和256就相對于256同余了,但是因?yàn)?位二進(jìn)制只能表示[0, 255]范圍的數(shù),再多就溢出了,256是沒法表示的。我們還是以-1舉例子:
取c為1,那么
0和256同余,即0000 0000
和1 0000 0000
同余,溢出位1
被舍去,就變成了0000 0000
。
負(fù)數(shù)的補(bǔ)碼數(shù)的表示范圍為[-128, 127],這個-128是怎么來的呢?它的補(bǔ)碼表示是1000 0000
。要解答這個問題,得看看原碼和反碼中的0
的表示。
先看看原碼,左邊第一位是符號位,說明它把0~255個數(shù)分成了兩半,分別是0~127, 128~255。其中,0000 0000
和1000 0000
都表示0,所以它只能表示255個數(shù)。即,-127~127.
類似地,反碼中的0000 0000
和1111 1111
也都表示0,其中1000 0000
表示-127,所以它也只能表示255個數(shù)。即,-127~127.
但是補(bǔ)碼就不同,它里面的0就只有一個0000 0000
,而-128和128相對于256同余,但是128(1 0000 0000)在8位機(jī)器上沒法表示,所以干脆把
1000 0000
直接拿來當(dāng)-128,可想而知,這個數(shù)是沒法通過原碼取反加1計(jì)算出來的,因?yàn)樗鼘?yīng)的原碼并不存在(整數(shù)128是不存在的)。
補(bǔ)碼的計(jì)算規(guī)則是怎么來的?
正數(shù)的補(bǔ)碼就是其本身;
負(fù)數(shù)的補(bǔ)碼是在其原碼的基礎(chǔ)上, 符號位不變, 其余各位取反, 最后+1
先說說反碼計(jì)算方式的由來。以-1為例,其原碼表示如下:
1000 0001
那么為何對它符號位之外進(jìn)行取反,就得到了它相對于255(1111 1111
)的反碼呢?
我們用255減去-1的絕對值,試試看
1111 1111
- 0000 0001
-----------
1111 1110
1111 1110
就是254,它和1000 0001
保留符號位后各位取反的結(jié)果一致。這是巧合嗎?顯然不是,因?yàn)?code>1111 1111是8位中最大的數(shù),所以就不存在借位操作,那么各位減下來,位是0的自然得到1,位是1的自然得到0.
-1和254相對于255同余,所以-1是254的補(bǔ)(One's complement)。不信的話,可以去clojure repl中運(yùn)行下面的表達(dá)式。
(mod -1 255) #=> 254
知道到了反碼的求值方法的由來,我們不難推斷出補(bǔ)碼的計(jì)算方法。因?yàn)?56是255+1,取反就是用255減去該數(shù),那么用256減去該數(shù),也就等價于255減去該數(shù)再加一。
計(jì)算機(jī)是如何區(qū)分unsigned int和int的?
我們已經(jīng)知道了計(jì)算機(jī)存儲數(shù)據(jù)全部用的補(bǔ)碼的形式,所以從內(nèi)存中拿出來的數(shù)就是補(bǔ)碼,那么-1的補(bǔ)碼是1111 1111
,也就是數(shù)2^8-1=255
.
如果說unsigned int的存儲形式和int的一致,那意味著int負(fù)數(shù)轉(zhuǎn)換成unsigned int的值就是它補(bǔ)碼的字面值。我們使用Solidity語言程序驗(yàn)證,在驗(yàn)證之前,先要安裝ganache-cli和solidity-repl.
$ ganache-cli
...
$ solr
Welcome to the Solidity REPL!
> int8 c = -1
> uint8 d = uint8(c)
> d
255
-1的補(bǔ)碼是1111 1111
,也就是十進(jìn)制的255,所以從結(jié)果中不難得出如下結(jié)論:在計(jì)算機(jī)中,數(shù)的存儲和表示是分開的,存儲的是補(bǔ)碼,計(jì)算過程也使用補(bǔ)碼,但是最后的表示由程序員來決定。所以毫不夸張地說,程序員是規(guī)則的締造者,也是規(guī)則的解讀者。
順帶一提
solidity中的int和uint是成對的,而且從8, 16, 24, ..., 256,一共有32個。正確性可以通過它的詞法分析程序得出來。
//solidity/liblangutil/Token.cpp
if (0 < m && m <= 256 && m % 8 == 0 && positionX == _literal.end())
{
if (keyword == Token::UInt)
return make_tuple(Token::UIntM, m, 0);
else
return make_tuple(Token::IntM, m, 0);
}