眾所周知,老婆餅裡是沒有老婆的,夫妻肺片是裡沒有夫妻的,那麼四則運算中可以沒有 +、-、×、÷ 這四個運算符嗎?當然是可以的。本文會先從基礎知識介紹起,再通過代碼實現,最後,我們還可以探討一下計算機運算器的起點及其哲學。
1. 位運算實現四則運算的原理及其基礎知識1.1 原碼、反碼與補碼眾所周知,計算機的世界是由 0 和 1 組成的,我們平時也會稱其為一個「二進位」的世界。一個數在計算機中的二進位表示,叫作這個數的機器數,其中,最高位表示該數的符號,正數為 0,負數為 1。另外,我們也將帶有符號位的機器數對應的真正數值稱為機器數的真值。
原碼(true form):原碼就是我們上段提到的真值的概念。直觀點講,例如,用 8 位二進位表示一個數,+11 的原碼為 0,0001011,-11 的原碼為 1,0001011。
反碼(Inverse code):機器數是正數時,反碼和原碼是一樣的;為負數時,反碼就是原碼除符號位外,其他位按位取反。直觀來說,+0 和 -0 的反碼,+0 的原碼是 0,0000000 ,則反碼為 0,0000000;-0 的原碼為 1,0000000,則反碼為 1,1111111 。
補碼(two's complement representation):在計算機系統中,數值一律用補碼來表示和存儲(請記住這一點)。原因在於,使用補碼,可以將符號位和數值域統一處理;同時,加法和減法也可以統一處理。
介紹完三者的概念,我們來演示一下,補碼是怎麼計算得來的。
首先,我們要先介紹一下「模」的概念:「模」是指一個計量系統的計數範圍,如過去計量糧食用的鬥、時鐘等。計算機也可以看成一個計量機器,因為計算機的字長是定長的,即存儲和處理的位數是有限的,因此它也有一個計量範圍,即都存在一個「模」。如:時鐘的計量範圍是0~11,模=12。表示n位的計算機(32位,64位)計量範圍是 0~2^n -1,模即為 2^n 。「模」實質上是計量器產生「溢出」的量,它的值在計量器上表示不出來,計量器上只能表示出模的餘數。任何有模的計量器,均可化減法為加法運算。
假設當前時針指向8點,而準確時間是6點,調整時間可有以下兩種撥法:一種是倒撥2小時,即8-2=6;另一種是順撥10小時,8+10=12+6=6,即8-2=8+10=8+12-2(mod 12)。在12為模的系統裡,加10和減2效果是一樣的,因此凡是減2運算,都可以用加10來代替。若用一般公式可表示為:a-b=a-b+mod=a+mod-b。對「模」而言,2和10互為補數。實際上,以12為模的系統中,11和1,8和4,9和3,7和5,6和6都有這個特性,共同的特點是兩者相加等於模。對於計算機,其概念和方法完全一樣。n位計算機,設n=8,所能表示的最大數是11111111,若再加1成100000000(9位),但因只有8位,最高位1自然丟失。又回到了 00000000,所以8位二進位系統的模為 2^8 。在這樣的系統中減法問題也可以化成加法問題,只需把減數用相應的補數表示就可以了。把補數用到計算機對數的處理上,就是補碼。即計算機中的 a-b 等於 a 加上 b 的補碼。
原碼求補碼,以及補碼求原碼,大家感興趣的話可以自己手動算一下,這裡直接給結論。
1.1.1 原碼求補碼正數:正數的補碼與原碼相同。
負數:負數的補碼,將其原碼除符號位外的所有位取反(0變1,1變0,符號位為1不變)後加1。
1.1.2 補碼求原碼⑴如果補碼的符號位為「0」,表示是一個正數,其原碼就是補碼。
⑵如果補碼的符號位為「1」,表示是一個負數,那麼求給定的這個補碼的補碼就是要求的原碼,即除符號位外取反後加1。
1.2 四則(加減乘除)運算的原理1.2.1 加法加法是運算中最簡單的一個。小學我們學的加法,都是十進位的,個位加個位,十位加十位,逢十進位,各個位上的數值再加上進位就得到了結果,「和」。同理,計算機二進位的世界也是一樣的,無非是 1 + 0 = 1,1 + 1 = 0,0 + 0 = 0,逢 2 進位。學過邏輯運算的應該都知道,前面的這種等式可以使用 「異或」 來進行實現,運算符為 ^,也是計算機中位運算的一種,然後再記錄進位就可以了。
1.2.2 減法既然解決了加法,那麼減法就很容易了,a - b = a + (-b),而上面提到的補碼的知識在這裡就有用了,求 b 的反碼加1 即為 -b 的補碼。這裡我們用到的運算為取反,位運算符為 ~b
1.2.3 乘法乘法的含義即為被乘數個乘數相加,說白了還是加法。
1.2.4 除法除法的含義即為不停地用除數去減被除數,直到被除數小於除數時,此時所減的次數就是我們需要的商,而此時的被除數就是餘數。
代碼實現了解完基本原理,寫代碼就是很簡單的事了。註:這裡的代碼只是簡單地實現了一下原理,感興趣的同學可以寫出更優的代碼,也可以使用其他語言實現一下。
1. 加法/**
* 實現 a + b
*
* @param a
* @param b
* @return a 與 b 和
*/
public static int add(int a, int b) {
int sum = 0;
// 進位
int carry;
while (b != 0) {
sum = a ^ b;
// 對應位和的進位,既然是進位,就要整體左移一位
carry = (a & b) << 1;
a = sum;
b = carry;
}
return sum;
}
2. 減法/**
* 實現 a - b,其實就是 a + (-b)
*
* @param a
* @param b
* @return a - b 的差
*/
public static int subtract(int a, int b) {
// 引入 c = -b
int c = add(~b, 1);
return add(a, c);
}
3. 乘法/**
* 實現 a * b,即將被乘數累加乘數次得到的結果,先計算絕對值累加,再求符號
*
* @param a
* @param b
* @return
*/
public static int multiply(int a, int b) {
// 將乘數和被被乘數都取絕對值
int multiplier = a < 0 ? add(~a, 1) : a;
int multiplicand = b < 0 ? add(~b, 1) : b;
// 計算絕對值的乘積
int product = 0;
int count = 0;
while (count < multiplier) {
product = add(product, multiplicand);
count = add(count, 1);
}
// 計算乘積的符號
if ((a ^ b) < 0) {
product = add(~product, 1);
}
return product;
}
4. 除法/**
* 計算 a / b 的值,即不停地用除數去減被除數,直到被除數小於除數時,此時所減的次數就是我們需要的商,而此時的被除數就是餘數。注意符號。
*
* @param a
* @param b
* @return
*/
public static int divide(int a, int b) {
// 對被除數和除數去絕對值
int dividend = a < 0 ? add(~a, 1) : a;
int divisor = b < 0 ? add(~b, 1) : b;
// 對被除數和除數的絕對值求商
int remainder = dividend;
int quotient = 0;
while (remainder >= divisor) {
remainder = subtract(remainder, divisor);
quotient = add(quotient, 1);
}
// 求商的符號
if ((a ^ b) < 0) {
quotient = add(~quotient, 1);
}
return quotient;
}
「抽象」與「封裝」從上面的代碼我們可以看出,當有了加法之後,減法、乘法、除法皆可以在之前的基礎上完成運算,這裡我們還可以抽離出幾個方法,比如,求一個數的相反數:
//求a的相反數:將各位取反加一
int negative(int a) {
return add(~a, 1);
}這樣,減法的代碼就可以寫作:
public static int subtract(int a, int b) {
return add(a, negative(b));
}是不是比之前的代碼簡潔了許多。我們把求反碼的過程「封裝」成一個函數,然後將這個函數「抽象」為取相反數,這樣我們就可以忽略封裝的內容,直接使用抽象出來的方法就可以了。就像我們首先封裝了加法,其他運算也是直接使用加法的函數,而不去深究加法具體是怎麼實現的,這就是 抽象 的好處和魅力,它讓我們不用去管底層細節,把精力用來構建更複雜的系統。
眾所周知,假如你知道的話,計算機硬體上由控制器、運算器、存儲器、輸入輸出設備組成,其硬體實現也從最開始的繼電器、真空管,再到電晶體,計算機內部電路的複雜程度和計算能力可能遠超過我們的想像。不過,再複雜的東西也都是由最簡單的組件構成的,小組件可以構成大一點的組件後,大一點的組件可以組成更大的組件,其思想就是利用 「抽象」 和 「封裝」 來降低我們對於複雜度的認知。至少在計算機和軟體工程領域如此,如果你跟我說畢卡索的 「抽象」 藝術,那當我沒說。
那麼想必你也知道,電路中有電流流過,我們就把它認為是「1」,也可以表示為 「true」,電路中沒有電流流過,我們就把它認為是「0」,也可以表示為 「false」,那麼該如何來表示和計算呢?在數學領域,有一整個數學分支,專門處理「真」和「假」,而且它已經解決了所有法則和運算,稱為「布爾代數」(Boolean Algebra)!與常規的計算不同,布爾代數的運算並不是加減法,而是邏輯運算,其中有三個基本的邏輯運算:NOT 、AND 和 OR。
NOT:NOT 運算有一個輸入,一個輸出,輸入為 true 時,輸出 false;輸入為 false 時,輸出為 true。我們可以列出其「真值表」以及畫出其電路實現。
AND:AND 運算有兩個輸入,必須兩個輸入都為 true,輸出才為 true;否則,輸出為 false。
OR:OR 運算也有兩個輸入,當兩個輸入有其中一個為 true 時,輸出便為 true;否則,輸出則為 false。
接著,我們可以進行一次抽象。可以將 NOT 門電路、AND 門電路、OR 門電路抽象成如下符號,電路本身的電晶體和電線還是在的,只是換了一種表現方式:
然後我們就可以用這些抽象出來的符號,構建更大的組件,而且也不會變更很複雜。比如,我們可以用來構建另外一個很重要的布爾操作,「異或」(XOR-Exclusive OR),代碼裡的位運算符為 ^ ,就是我們實現加法的那個。其真值表為:
以及我們可以按照下面的步驟通過其他的邏輯門電路實現一個異或的電路:
然後我們可以再進行一次抽象,將異或用下面的符號表示:
這樣,我們就可以把 「異或」 放入工具箱,下一次便可直接使用它來構建其他組件,而不用顧慮 XOR 具體用了幾個門電路,以及這幾個門電路又是怎麼用電晶體拼的,電子是怎麼流過半導體的。
總結本文先是不用 + 、- 、× 、÷ 四種符號實現了簡單的四則運算,來說明了計算機底層是如何完成計算這一任務的。之後又簡單介紹了一下邏輯運算,與邏輯門電路的實現,給大家初步展示了一下計算機中 「抽象」 的藝術。總之,在抽象的基礎之上,計算機工程師在設計處理器時,很少會在電晶體層面上思考,而是用更大的組件思考,比如邏輯門什麼的;開發工程師在開發功能時,也極少會考慮邏輯是怎樣在物理層面實現的,而是會專注於業務邏輯實現。以致於當前最流行的 Serverless 、Faas 等等雲計算相關的技術,大多也得益於這種思想。希望大家也會有所啟發。
連結•用基本位運算實現加減乘除:https://www.cnblogs.com/kiven-code/archive/2012/09/15/2686922.html•相關代碼(Java 版):https://github.com/lq920320/algorithm-java-test/blob/master/src/test/java/other/calculator/Calculator.java•【計算機科學速成課】:https://www.bilibili.com/video/BV1EW411u7th•阮一峰 XOR 教程:https://mp.weixin.qq.com/s/dVOyUMaJRRIsND0yz4Llvg