前言
最近在看《Computer System: A Programmer's Perspective》,學會了很多基礎性的知識,于是總結出來與大家分享。
位與二進制
在現實生活中,我們會用紙和筆來記錄數據,比如在之前智能手機還沒有普及的年代,還有相當一部分人使用小本本來記錄電話號碼,顯然電話號碼作為一種數據,記錄在紙上。
那么在計算機中是如何記錄數據,表達信息的呢?
計算機使用一個序列的位(bit)來記錄數據。
一個位中存儲著一個二進制數字,什么是二進制呢?二進制簡單來說就是逢二進位的一種進制,人類最常用,最直觀的一直使用著十進制,可用的符號為:0,1,2,3,4,5,6,7,8,9,總共有10個符號,二進制則只需要兩個符號0和1。對機器來說,使用二進制是非常方便的一種形式,因為二進制只需要兩種符號,也就是說,機器只需要能夠維持兩種不同的狀態來對應這兩種符號即可,比如高電壓和低電壓,通電和斷電等等。所以,對計算機來說,使用二進制(維持兩個狀態)是非常有效的方式。
計算機使用一連串的位來記錄數據,比如1位,那么這個位上只能表示0或1,通常1位的數據幾乎沒有什么作用。在現在的計算機中,使用8個位來作為一個基本的單位,稱為字節(byte),那么它寫出來應該是這樣的:
//8個位都是0,對應十進制數字0,中間空開一個空格,四位四位的寫在一起是為了可讀性
0000 0000
//右側的一般稱為最低位,左側的稱為最高位,最低位加1,對應十進制中的1
0000 0001
//最低位繼續加1,1+1=2,因為是二進制,必須進位了,對應十進制數字2
0000 0010
0000 0011
...
//8位二進制最大值,對應十進制2^8-1=255
1111 1111
以上就是一串從0開始遞增的二進制序列。
十六進制
剛剛說完了二進制數,現在簡單介紹一下十六進制數,二進制數與十六進制數之間有非常巧妙的關系。
十進制需要10個符號:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
二進制需要2個符號:0, 1
十六進制需要16個符號:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f
其中,a-f分別對應著十進制中的10, 11, 12, 13, 14, 15
十六進制數值中的英文字母是不區分大小寫的。
現在,讓我們來考慮4位二進制數
0000(2) -> 0(10) -> 0(16) //括號中表示進制
一個4位的二進制數最小為0000
,也就是十進制中的0
,也是十六進制的0
。那么4位二進制最大的值為1111
,那么它的值為
也就是說,四位二進制能表示數的范圍區間為[0, 15],這剛好是1位16進制數所能表示的數值范圍。
四位二進制數能搞好使用一位十六進制數來表示。
于是,當計算機中的數值使用二進制表示時,通常會出現成片成片的010101……,這看起來非常頭疼,非常容易讓人看錯,但是,我們可以從低位開始,四個四個的表示為十六進制數。如下所示:
0000 0000(2) -> 00(16)
0000 1111(2) -> 0x0f //通常,"0x"前綴用來指明是一個十六進制數
1111 0001(2) -> 0xF1 //十六進制的字母是不區分大小寫的,注意這里大寫的F
100 1111(2) -> 0x4f //注意!這里的二進制數只有7位,通常我們從最低位開始轉換成十六進制數,高位在沒有指明的情況下,使用0補齊
0100 1111(2) -> 0x4f //上一行的二進制數高位補齊0的情況
有了十六進制,我們就可以方便簡潔的表示非常多位的二進制數,有了更高的可讀性。
整數
整數又分為有符號與無符號的區別,無符號整數即是大于等于0的整數。
無符號整數的表示
無符號整數的表示是基于最原始的二進制表示數據的辦法,也就是說,給定n位二進制序列,它所能表示的數值范圍是。
是怎么來的呢,其實很簡單,比如我們給出一個字節(8位)來表示一個整數,
0000 0000
,它能表示的最小值應該是0了,也就是8位全是0,那么最大值呢?當然是8位全是1了,也就是1111 1111
,現在不妨給它加個1,那么它會變成9位的二進制數值1 0000 000
,此時,這個9位的二進制數的值為,那么8位最大值當然就是
了。
所以,無符號整數在計算機中的表示,就是簡單的對位序列的數值理解即可。
有符號整數的表示
通過位序列來表示有符號整數是非常巧妙的,稱之為補碼編碼的方式來表示有符號整數。所謂使用補碼來表示有符號數,實際上對二進制序列并沒有什么特別大的處理,它仍然是010101……這樣的一串東西,只是我們用一套稱為“補碼”的規則來理解這串位序列而已。
補碼就是將位序列的最高位解釋為負權。
比如,給定位序列,一個字節1000 0001
,如果它是表示無符號整數,它的值是多少呢?很簡單,那么如果我們用補碼來理解它呢?最高位是負權,也就是
。
補碼會將最高位理解為一個負數,直觀點來看,就是當最高位是1的時候,是一個非常大的負數加上后面的正整數,后面的正整數越大,這個負數越小,越靠近0,當最高位是0的時候,那么負權整個都是0,剩下的幾位如同無符號整數一樣表示正整數。
我們用一串遞增的序列來理解。
0000 0000 -> -0 + 0 = 0
0000 0001 -> -0 + 1 = 1
...
0111 1110 -> -0 + 126 = 126
0111 1111 -> -0 + 127 = 127
1000 0000 -> -128 + 0 = -128
1000 0001 -> -128 + 1 = -127
1000 0010 -> -128 + 2 = -126
...
1111 1110 -> -128 + 126 = -2
1111 1111 -> -128 + 127 = -1
通過上述遞增的序列,你會發現,8位二進制能表示的有符號整數的范圍是,我們可以用數學的語言來描述一個n位的二進制能表示的有符號整數范圍是
。
有符號數與無符號數的相互轉換
通常在高級程序語言中,有無符號整數的相互轉換是不改變位序列的,只是換了一種“解析”方式去解釋位序列,比如說1000 0000
是一個無符號數,那么它的值是128
,如果我們把它轉換成一個有符號數,位序列不變,只是用補碼的方式去理解它,那么它的數值就變成了-128
了。
我來用一張圖片說明得更清楚一些。
如上圖所示,在數據大小為8位的情況下,它的低7位所表示的數值范圍
在我們寫代碼的時候一定要注意這一點。
擴展一個數值的位
如果我們要將一個8位的數據放到16位的容器中去,那么我們需要對數據進行擴展, 也就是我們需要確定新的高8位應該是放0還是放1。
這個問題很簡單,對于無符號數的擴展,只要簡單的在高位上補充滿0即可,這種方式稱為零擴展。對于有符號數,只需要在高位補充滿1即可,這種方式稱為符號擴展。
整數的加法計算
整數的計算分為,無符號整數加法,有符號整數加法,無符號與有符號整數混合的加法。
對于無符號整數的計算,只是單單從位級上考慮就行了,但是當兩個二進制數相加后,最高位向前進1了,那么就會產生溢出,本來應該變成一個更大的數,結果卻變小了。
對于有符號數的計算,同樣的也是從位級上進行加法計算,一樣的,最高位如果向前進一了,一樣會產生溢出。比如對于8位的數據,-128 - 128 -> 0
,我們在位級上進行考慮
//這是一個豎式
1000 0000
1000 0000
----------
0000 0000
對于有符號數與無符號數混合的表達式,一般需要查看編譯器是如何處理這個問題的,有可能是將有符號數轉成無符號數再進行計算,也有可能是將無符號數轉成有符號數再進行計算。這個問題跟整型與浮點數相加是類似的問題,還是看編譯器/虛擬機是具體如何解決這個問題的。
浮點數
前面所說的有符號整數與無符號整數,都屬于定點數,就是小數點固定的數,而浮點數,即小數點是可以浮動(變化)的數。自從我聽到浮點數這個概念,在一個很長的時間里,我都以為浮點數就是指小數,其實不是這樣的,浮點數并不是狹隘的說一定要表示為小數(如123.45),應該更準確的理解為小數點的位置不是固定的,也就是說,這種數的“表示/解析”方法是可以表示小數點在不同位置的數的。
二進制小數
在解釋浮點數之前,我們先要知道一下二進制的小數是什么情況。
先看一個二進制整數的例子,如101
,那么它的十進制數值為,那么,當二進制數值有小數點時,
101.101
,它的十進制數值為
可以發現,0.1(2),0.01(2),0.001(2)……二進制小數每一位能表示為,
,
……因此它要表示某一個十進制的小數,需要用這些部分累加在一起實現。
IEEE浮點標準
在生產環境中,一個基本的浮點數都是32位的,不會像上文中一直使用的8位來當數據的大小,IEEE浮點標準中就規定了這32位應該如何使用。
它將位序列分為三個部分來理解,
第一部分:符號,決定這個數是正數還是負數,一般用1表示負數,0表示正數。
第二部分:尾數,是一個二進制小數。
第三部分:階碼,是對浮點數的加權。
可以這樣去理解,有點像科學計數法,比如:
100,我們可以記為,
0.257,我們可以記為
257,可以記為
IEEE標準中,給定了32位與64位浮點數,各個部分的格式。
32位浮點數:
最高位:符號位(1位)-階碼(8位)-尾數(23位):最低位
64位浮點數:
最高位:符號位(1位)-階碼(11位)-尾數(52位):最低位
我們先假設一個8位的浮點數,并通過將它的數值列在一個表格中來觀察學習浮點數的標準是如何工作的。
8位浮點數,我們設定最高的1位是符號位,0表示正數,1表示負數,接下來的4位表示階碼,最低3位表示尾數。請看下表。
上表,只列出了符號位是0的情況。
規格化數與非規格化數
我們注意A列的描述,浮點數大體上分為三種情況,非規格化數,規格化數,其他值
非規格化數的特征是,階碼段的位都是0。
規格化數的特征是,階碼段不全為0,也不全為1。
其他值的特征就是,階碼段全為1。
指數部分
然后,我們注意一下D列,有一個偏置值的概念,它的值是,k的值是階碼的長度(位寬),在我們自定義的8-bit浮點數中,k的值為4,所以偏置值是7。
階碼E是分兩種情況的。
當這個浮點數是非規格化數的時候,
當這個浮點數是規格化數的時候,
e是階碼段的4-bit位序列所表示的無符號數。
所以,表格中E列指的是4-bit位序列按無符號數解析的十進制無符號數。
而F列,則是按是否是規格化數來決定的值。這個偏置值的設定與補碼負權的設計是非常相似的。
最終指數部分就是了。
小數部分
小數部分M是由最后三位決定的,它同樣是分情況的。
當這個浮點數是非規格化數的時候,位序列BBB應當理解為0.BBB,也就是整數部分為0的二進制小數。
當這個浮點數是規格化數的時候,位序列BBB應當理解為1.BBB,也就是整數部分為1的二進制小數。
此時,對照表格的H列與I列,即可明白它們的含義。
浮點數的值
最終,這個浮點數的值就這樣得出來了。sign是第一位決定符號的。
抽象數據模型(Abstract Data Models)
最近無意間看到一個這樣的概念,與計算機中的數有關,就在這里提及下。
應用與操作系統都有一個抽象數據模型,大部分應用都沒有顯式的表現出這個模型,但是它會影響到代碼的編寫,在32 bits programming model(ILP32)上,integer, long, pointer都是32 bits的,大部分開發者都沒有意識到這一點。
現在系統擴展到64 bits,如果把所有的數據類型都擴展到64位是非常浪費的,因為很多應用并不需要真的用到64位那么大的數據格式,但是pointer卻需要擴展到64位,所以在LLP64/P64上,pointer被擴展到64位,其他的仍然保持32位。
以上內容翻譯自Abstract Data Models
抽象數據模型指定了編程語言中幾個基礎數據類型的大小。
比如LP64(可能是64-bit Leopard的縮寫)是使用在64位OSX系統或者Linux系統上的,它指定了integer為32位,long是64位,pointer是64位。
還有LLP64,這是windows 64位操作系統所選擇的ADM,它的integer/long/pointer分別使用的是32/32/64位。
更細致的說明與討論,我已經整理好了參考資料給大家。
參考資料
- 《64-bit data models》wiki上的解釋。
- 《Abstract Data Models》這篇文檔介紹了Abstract Data Model這個概念,提及了ILP32與Win64用的LLP64。
- 《Windows Data Types》
- 《The New Data Types》這兩篇文檔列舉了數據類型的大小。
- 《64-Bit Programming Models: Why LP64?》這篇文檔詳細比較,討論幾種不同的抽象數據模型。
- 《深入理解計算機系統》
有看不懂的地方請給我說,我再添加更詳細的解釋;有講得不正確的地方還歡迎大家指正與討論:D