我們現在的位置:在 ALU 的加法器裡
在上一篇你認識了ALU,把它當成資料路徑的計算機:一個吃進兩個運算元加上一組控制碼、再回傳結果的方塊。我們把它的「加法」線路當成黑盒子。現在我們把盒子打開。誠實的好消息是:ALU 在整數運算上做的幾乎每件事,都歸結為一個電路——加法器——再加上它周圍幾個便宜的小手段。減法其實是偽裝過的加法;比較則是把結果丟掉的減法。只要你懂加法器與它的旗標,你就懂了這台機器的算術核心。
也請回想邏輯那一階:全加器是一個小小的組合邏輯方塊,有三個一位元輸入——兩個運算元位元 a 與 b,加上一個進位輸入——以及兩個輸出:一個和位元與一個進位輸出。把 n 個串起來,每一個的進位輸出餵給下一個的進位輸入,就得到一個 n 位元的漣波進位加法器。這條鏈,其實就是你小時候學的直式加法:從個位往十位進位,只是換成二進位、而且只有 1 與 0。
二進位加法,一欄一欄來
二進位加法完全照著十進位加法的規則走,只是九九乘法表小得多:0+0 是 0、0+1 是 1、1+1 是 0 進位 1,而 1+1+1(當有進位進來時)是 1 進位 1。你從最右邊那一欄開始,也就是最低有效位元,把兩個位元加上進來的進位相加,寫下答案的低位,再把產生的進位往左推一欄。進位向左漣漪傳遞,正是基本加法器叫做「漣波進位」的原因。
看一個具體例子:8 位元裡的 87 + 58。寫成二進位是 01010111 + 00111010,一欄一欄相加得到 10010001,且沒有任何進位離開最高欄。現在好好盯著這個結果,因為它帶著一個警告,接下來幾節會把它拆開。把這八個位元讀成無號整數,答案是完全正確的 145。但把同樣這些位元讀成有號的二補數——8 位元只到 127——145 就超出範圍了,而 10010001 這組樣式其實代表 -111。一樣的線、一樣的加法,卻有兩種判決。位元本身無法告訴你你想要哪一種解讀;那是你的工作,ALU 只負責把兩種警報都舉起來。
免費的減法:取補數再相加
這就是你在資料那一階學到的二補數方案帶來的回報。要算 a - b,硬體不會另外蓋一個減法器。它算的是 a + (-b),而在二補數裡把 b 取負意思就是「把 b 的每個位元反轉,然後加 1」。反轉是免費的——用 NOT 閘把每條線取反就好。「加 1」也是免費的,因為加法器最右欄本來就有一個進位輸入:餵它一個 1 即可。所以減法重複使用同一個漣波進位加法器,只多了兩個便宜的修改,這也是為什麼前面那篇承諾減法會「免費」得來。
這種共用是整個計算機算術裡最令人滿足的點子之一,也解釋了一個深層的設計抉擇。業界放棄符號-數值表示法與一補數、改用二補數的真正理由,不是紙上的優雅——而是二補數能讓一個加法器靠一條額外控制線同時服務兩種運算,沒有特例、而且只有一個零。比較也順帶冒出來:要測 a 是否等於 b、或 a 是否小於 b,ALU 就做減法再檢查旗標,根本不必保留那個差。
兩個警報:進位與溢位
現在來到大家最容易搞混的部分。做完一次加法,ALU 會設定好幾個條件旗標,其中兩個聽起來很像、意思卻完全不同。進位旗標單純記錄是否有進位離開最高位的那一欄。它是無號運算的正確警報:如果你把兩個無號數相加而有進位逃出最高位元,真正的和需要的位元比你擁有的多,所以無號結果繞回去了。進位講的是在無號世界裡空間不夠。
溢位旗標是有號二補數運算的警報,而且是完全不同的測試。有號溢位發生在真正的數學結果落在有號範圍之外——以 8 位元來說,落在 -128 到 127 之外——即使根本沒有進位離開最高位元也算。破綻在正負號:當你把兩個同號的數相加、卻得到一個反號的結果時,正好就是溢位。兩個正數加出「負」的、或兩個負數加出「正」的,在真實數學裡不可能發生,所以位元一定是繞回去了。
8-bit, signed two's complement (range -128..127)
64 + 64 : 01000000 + 01000000 = 10000000 = -128
same sign in, opposite sign out -> OVERFLOW set
carry out of MSB = 0 -> CARRY clear
255 + 1 (read as unsigned 0..255):
11111111 + 00000001 = 00000000 = 0 (wrapped)
carry out of MSB = 1 -> CARRY set
read as signed -1 + 1 = 0 -> OVERFLOW clear
Carry -> unsigned wrapped. Overflow -> signed wrapped. Independent!把那段追蹤讀兩遍:這兩個旗標是真正獨立的。第一行溢位響、進位不響;第二行進位響、溢位不響。硬體用一個便宜的方式算溢位:把進入最高位的進位與離開最高位的進位做 XOR——當這兩者不一致,正負號就被弄壞了。這裡關鍵的誠實之處是:矽片從不替你決定哪個警報才重要。C 編譯器在有號加法後插入的檢查看溢位旗標;在無號加法後則看進位旗標。同一個加法器、同樣的位元,但語言的型別系統決定哪一個旗標才是真正的警告。
誠實的意外,以及接下來通往何處
有幾個值得記住的真相。第一,硬體溢位預設是沉默的:位元就這麼繞回去、旗標被設好,除非有東西去讀那個旗標,否則程式就帶著一個錯誤的數字繼續航行。許多真實的臭蟲與安全漏洞正是如此——某個長度或索引繞過了它的最大值。第二,二補數裡有一個必須尊重的不對稱:最負的值(8 位元是 -128)沒有正的雙胞胎,所以對它取負會溢位、又把 -128 還給你。把 10000000 反轉再加 1,你又落回 10000000;數學沒地方擺 +128。
整數算術的其餘部分都從這一個加法器長出來。乘法骨子裡是一疊位移後的加法(移位相加法,再由 Booth 演算法與陣列乘法器加速);除法是反覆的相減與位移,眾所周知是四種運算裡最慢的,因為每一步都要等上一步。浮點數則把有號整數算術包進處理指數、正規化與捨入的機構裡。但那每一樣都建立在你現在握住的東西上:一欄一欄相加的位元、被回收成「加補數」的減法,以及兩個誠實的小旗標,告訴你答案何時掉出了邊緣。