设计一个十进制纯机械乘法器,继续大数乘法
緣由
周六的一個下午和今天一個早上,終于寫完了本文。昨天上午用紙板子做了個簡單的機械行列選擇機,被問起為什么,我說我不喜歡電子的東西,我喜歡能hold住全場的,畢竟電子的東西我搞不定電池和各種門電路…自制發(fā)電機又沒有漆包線,好吧,拆馬達即可…馬達既可以發(fā)電,又可以被電驅(qū)動,你要是擔心自己搞不定足以發(fā)電的轉(zhuǎn)速,反著用減速齒輪不就是個加速齒輪嗎?
正文
上一篇文章描述了大數(shù)乘法的基本思路和我的一些思考:
計算機大數(shù)乘法引發(fā)的思考: https://blog.csdn.net/dog250/article/details/102520390
然而意猶未盡,本文繼續(xù)。
我想試試看怎么能設計一臺直接計算十進制乘法的機械裝置,不用任何通電元器件。大家都在玩互聯(lián)網(wǎng)的時候,我卻退回到了機械,僅僅覺得好玩。
本文將介紹一些更有意思的東西。
程序員寫文章,數(shù)學公式可以沒有,但一定要有代碼,不然會被笑話。所以,本文還是有完整代碼的。
我們還是先從一個簡單的問題開始:
- 計算機計算 “873456×3456876873456\times 3456876873456×3456876” 和 “3×43\times 43×4”,哪個更快?
試試看是最直接的了。我們先寫個代碼比較一下上述兩個乘法的計算耗時:
#include <stdio.h> #include <sys/time.h>int main(int argc, char **argv) {int i, j;long mul1, mul2, result = 0;int count;struct timeval start, end;unsigned long elapsed;count = atoi(argv[1]);mul1 = atol(argv[2]);mul2 = atol(argv[3]);gettimeofday(&start,NULL);// 為了稀釋掉定時器誤差,這里計算count次for (i = 0; i < count; i++) {result = mul1*mul2;}gettimeofday(&end,NULL);elapsed = 1000000 * (end.tv_sec-start.tv_sec)+ end.tv_usec-start.tv_usec;printf("long result=%d elapsed time=%ld us\n", result, elapsed);return 0; }然后我們分別用相對大的數(shù)字和相對小的數(shù)字來計算10000乘法:
[root@localhost ]# ./a.out 10000 873456 3456876 long result=67074368 elapsed time=23 us [root@localhost ]# ./a.out 10000 3 4 long result=12 elapsed time=24 us時間幾乎是一致的。可以簡單得說:
- 計算機并不會因為乘數(shù)的數(shù)位短而運行的更快。
這有悖于我們的直覺!在我們看來,越長的數(shù)字不是需要越多的步驟來計算嗎?比如我們用豎式計算。
事實上,我們之所以在豎式計算時會感覺數(shù)字越短計算越快是因為我們可以 “一眼” 識別到數(shù)字的長度。我們一眼就看完了整個數(shù)字,而計算機只能枚舉整個字長空間,才能確定數(shù)字的結(jié)束。
我們先來看一下CPU乘法器的結(jié)構(gòu):
非常簡潔完美,這是由二進制的性質(zhì)決定的:
- 在單bit 乘以任意數(shù)字bbb時,結(jié)果要么為0,要么為bbb本身。
如果看不懂上面的電路圖,沒關系,C代碼可以看懂即可。接下來我用C程序模擬一下這個乘法器的實現(xiàn):
#include <stdio.h>int main(int argc, char **argv) {int i, j;long lowest_bit;long a, b, result = 0;a = atol(argv[1]);b = atol(argv[2]);result = 0;for (j = 0; j < 8*sizeof(long); j++) {lowest_bit = b&0x1;if (lowest_bit == 0x1) {result += a;}a <<= 1;b >>= 1;}printf("result=%lu \n", result);return 0; }可以看到,必須把 sizeof(long) 全部check完之后,才能結(jié)束計算,無論你提供的乘數(shù)的長度是1字節(jié)還是8字節(jié)。
這只是一個引子,我們不僅僅可以得到 “計算機并不會因為乘數(shù)的數(shù)位短而運行的更快。” 的結(jié)論,更重要的是,我們又找到了一種計算大數(shù)乘法的方法,即:
- 類似硬件乘法器那般機械地完成整個計算,它暴露的是乘法過程的本質(zhì)。
CPU乘法器將上述基于二進制的算法硬化到了門電路里。之所以可以硬化到CPU里,完全是利用了NP/PN結(jié)的特性,它們恰好可以用通和斷表示1和0!
這里引出一個新的問題:
- 這個算法可以基于十進制的硬件實現(xiàn)嗎?
當然,這只是一個 “僅僅覺得好玩” 的問題,我并不會去真的做這么一個裝置。
這種裝置即便是做出來也只能欣賞一下機械之美,在現(xiàn)代電子計算機面前,再精致的機械計算裝置只能拿來看,就像我們在博物館看100多年前的戴姆勒梅賽德斯奔馳一樣。
十進制世界沒有硅可用,NP/PN結(jié)只有兩個狀態(tài),我們找不到現(xiàn)實世界中有十個狀態(tài)的物件,十個手指頭顯然不足以支持大規(guī)模計算,那么我們?nèi)绾卧煲慌_十進制的計算機器呢。
千百年以前人們就在思考這個問題了,從算籌,算盤,到帕斯卡加法機,差分機都是這方面的勇敢嘗試,閱讀和研究這些資源是技術考古學的事,但是今天,我想在現(xiàn)代的視角下從零設計一個十進制乘法器。
經(jīng)過一些思考,初步有了一個雛形,攜帶著我的一些思考過程,寫了本文。
首先能想到的一個現(xiàn)成的加法器就是機械鐘表。這是一個典型的3位60進制加法器!
機械鐘表作為一個加法器一直在持續(xù)做 遞增 加法操作,逢60進1,任意時間的時間值就是分別讀時針,分針,秒針的讀數(shù)疊加,讀作 “H時M分S秒” ,非常類似 “a百b十c” 。
拆開機械鐘表,我們發(fā)現(xiàn)這是一個三個不同大小的齒輪契合的傳動裝置,原理非常簡單,比葫蘆畫瓢,我們就能實現(xiàn)一個三位十進制加法器了,把刻度按照的1060\dfrac{10}{60}6010?歸一即可。
早期的帕斯卡加法器差不多也是這個原理。如果我們把這種齒輪契合傳動的加法裝置看作是一個 十進制模擬計算裝置 的話,我今天要介紹另一種截然不同的 十進制數(shù)字化計算裝置 。
先來類比二進制數(shù)字系統(tǒng)。
前面說了,二進制的硬件實現(xiàn)依賴了自然界的物質(zhì)硅。二極管,三極管組成的門電路刻畫在硅片上形成的芯片,是數(shù)字系統(tǒng)的基礎。
然而十進制沒有硅可用!怎么辦?
用齒輪啊!
齒輪是萬能的,我們照著門電路的樣子,用齒輪來堆積一個十進制乘法器如何?!
為了完成這個目標,先看芯片是如何進行二進制計算的。事實上,芯片不會計算,它只是一種晶體而已,之所以它看起來是會計算的,是因為人們設計了 恰好讓它看起來會計算的電路 ,這個設計背后的依據(jù)就是 “真值表” 。
換句話說,芯片并不知道 10&1110 \& 1110&11 意味著什么,它并不懂布爾代數(shù),芯片在計算10&1110\&1110&11時是通過查內(nèi)化到電路的一張表來完成的。
我們需要在裝置里內(nèi)化一張 “十進制真值表” 。
二進制的真值表基于二進制3個基本運算與,或,非而生成,而十進制基本運算又是什么呢?對于我們?nèi)硕?#xff0c;很顯然,十進制的基本運算就是一位數(shù)加法和一位數(shù)乘法,因此,可以認為, 九九加法表和九九乘法表就是十進制的真值表。
事實上,二進制的與,或,非操作就是二進制的乘,加,取相反數(shù)。
以九九加法表為例:
我們指望用齒輪來傳動上圖的結(jié)果輸出。
我們要用齒輪傳動統(tǒng)一處理查表結(jié)果,進位等問題,所以我們可以將加法表和乘法表的結(jié)果分拆成進位和結(jié)果兩部分,每一個部分都是一個一位數(shù),于是加法表和乘法表的樣子如下:
二進制電路可以用與門,非門,或門排列實現(xiàn)真值表,十進制如何內(nèi)置九九加法表和乘法表呢?
我們需要設計一個裝置,通過3維數(shù)組的前兩個維度定位操作數(shù),最后一個維度定位結(jié)果,比如,我們要找4+54+54+5的結(jié)果,那么它就是:
num1 = 4; num2 = 5; 進位 = add_table[num1][num2][0]; 個位 = add_table[num1][num2][1];乘法亦然。
因此很顯然,用 行列定位裝置 將會非常方便,行和列作為兩個輸入,其定位的矩陣單元作為輸出,可以確定結(jié)果的個位和十位,個位直接輸出,十位參與下一級的進位運算。
以 an...a2a1×b=cm...c1a_n...a_2a_1\times b=c_m...c1an?...a2?a1?×b=cm?...c1為例,看看一個多位數(shù)乘以一位數(shù)的乘法裝置最終是什么樣子:
如果讓電子元器件實現(xiàn)上述的乘法裝置,那再簡單不過了。電子裝置中,九九乘法表輸出的個位和十位可以輕而易舉的傳動到其它加法表的行或列選擇桿。
對于電子裝置而言,無論是模電還是數(shù)電,所有的問題都可以轉(zhuǎn)化為開關問題。想把力從A點傳到B點,只需要AB導線連接,B處裝控制動力的繼電器,A點接通電源即可。
對于純機械的方案,電子裝置的設計思路行不通,因為純機械裝置只認識樸素的牛頓定律,不借助簡單機械構(gòu)件,力只能橫平豎直地傳導,或者借助齒輪,輪軸等圓周力,徑向力,軸向力改變力的方向。
但這也正是純機械設計的魅力之所在,如果說數(shù)電領域二極管,三極管組成的與門,或門,非門可以組成一切,那么在機械領域,齒輪,輪軸,杠桿作為基本構(gòu)件,通過巧妙的組合,也能玩得五花八門。這也顯示了機械設計和電子設計的另一個很有意思的區(qū)別:
- 機械設計的特點在于,大部分人都能看懂,但是卻設計不出來,而電子設計則相反,沒有基礎的人根本看不懂,而一旦有了基礎,電子設計并不是很難。
思考加法,乘法矩陣的行選列選機制花了比較久的時間,我希望的效果是 行列選擇無狀態(tài)。 意思是說,無論先選擇行還是先選擇列,均可以驅(qū)動行列交叉處的傳動桿向下伸出。這對于操作而言非常友好,免除了任何時序的依賴,比如對于計算2356×82356\times 82356×8而言,我可以以任何順序按下2,3,5,6,82,3,5,6,82,3,5,6,8這五個數(shù)字的行列選擇桿,唯一的約束就是888要作為行選,而其它數(shù)字作為列選。
下面是我的一個設計示意圖(沒有機械制圖軟件,只能這樣了)。
我們看到,只有一根傳動桿輸出向下的運動,而根據(jù)加法表和乘法表可以看到,實際上有兩個輸出,分別對應個位和十位,這兩個輸出分別要驅(qū)動兩個加法表的列選擇,如何讓一個輸出變成兩個呢?
很簡單,用一個齒輪就可以了:
接下來讓我們看看完整的1位乘1位裝置的局部示意圖:
上圖中,還有一個細節(jié)沒有標出。我們知道,乘法表或者加法表矩陣中某個元素的輸出位可能是相同的,比如 1×3=[0,3]1\times 3=[0,3]1×3=[0,3] 和 7×9=[6,3]7\times 9=[6,3]7×9=[6,3],個位都是3,因此 (1,3)(1,3)(1,3),(7,9)(7,9)(7,9) 這兩個單元的輸出是相同的,能不能合并它們呢?
很難!因為嚙合齒輪組是耦合在一起的,解耦非常難,需要十分精妙的trick,每一步轉(zhuǎn)換都要損失傳動比,為了讓多個輸出相互不影響,最簡單的方案就是直接并聯(lián)輸出它們,因此對于靠乘法裝置或加法裝置驅(qū)動的加法器的行列選擇,它們看起來是下面的樣子:
另一方面,考慮到無論是乘法表還是加法表,都是對稱的,因此關于對角線對稱的兩個單元是可以合并的。
先不考慮對稱合并,100個乘法單元會有100根傳動桿輸出,100根傳動桿每一根攜帶一個聯(lián)動齒輪,然后加法表1同樣會有多根傳動桿輸出用于驅(qū)動加法表2的行列選擇…這才是一位乘法單元,考慮到 齒輪不適合遠距離傳動 ,需要把如此多的零件集中在非常小的空間內(nèi)!要是做出來,絕對是精密儀器!
換個思路, 能不能把100根傳動桿換成一根轉(zhuǎn)動軸呢?設計單輸出!
- 用轉(zhuǎn)速和轉(zhuǎn)距來表示相應的輸出數(shù)字,比如轉(zhuǎn)303030度表示000,606060度表示111 …
在乘法單元里直接將輸出的數(shù)字轉(zhuǎn)換成不同大小的齒輪以驅(qū)動轉(zhuǎn)軸的轉(zhuǎn)程,這樣整個乘法表就只有兩根轉(zhuǎn)軸輸出了,在輸入到加法裝置的時候,需要將轉(zhuǎn)程轉(zhuǎn)換回對應的數(shù)字,即用不同的轉(zhuǎn)程驅(qū)動不同的行列選擇桿,這也是很巧妙的:
即便如此,雖然省掉了幾百根傳動桿,但是乘法表內(nèi)部的齒輪組卻復雜了很多,加上實現(xiàn)驅(qū)動齒輪的邏輯,依然需要非常精密的工藝。
嚙合齒輪組解耦邏輯非常復雜,解耦的最簡單方案就是各自使用獨立的齒輪,然而多級傳動對齒輪間的最大距離,精度以及潤滑的要求極高,特別是在狹小的空間內(nèi),有效傳動變得更加困難。
試想,為了完成整個計算,整個裝置的輸入僅僅是人手指的輕微推力,靠這個輕微推力驅(qū)動如此多的齒輪,本身就是對精度和潤滑的一個極大挑戰(zhàn)。
粗曠思維驅(qū)動下,可以考慮液壓,油壓這種變力傳動機制。
思考了一晚上,放棄了液壓傳動和氣壓傳動,能稱為藝術品的機械設計只用齒輪和杠桿即可完成!純精密機械發(fā)源于古代,當時基本的橡皮管都造不出來,所以只能利用容易制造的齒輪,杠桿這種來做剛體傳動,換句話說精密機械大部分都是剛體機械,就不要指望傳送帶,液動這種現(xiàn)代裝置了,設計一個乘法機,結(jié)果搞成煤礦就無心插柳了。所以說,帕斯卡加法機直到今天依然是藝術品。
對了,還有中國古代的核雕,不亞于精密機械之精妙,《核舟記》里講的。
這些都是精細的巧活兒…對于我等不是從事這個行業(yè)的人特別是搞IT互聯(lián)網(wǎng)的來講,只能望洋興嘆,根本那個時間來精心打磨。如果這個裝置真的做出來了,那必然是奢侈品。
我自己是機械科班生。2002級黑龍江科技大學機械工程系科班出身。
我試著用紙箱的紙板做了一個1×11\times 11×1矩陣,大早上五點起來搞的:
我試著對它進行操作,效果還不錯。這個裝置的最重要特征就是無狀態(tài),無論先選擇行,還是先選擇列,都能驅(qū)動紅色的小棍棍向下伸出:
說明還是可行的,這是邏輯上比較麻煩的部分,剩下的就是精密傳動部件的堆積了。做是做不出來了,真的好難。
我特意咨詢了機械加工的費用,200mm尺寸內(nèi)的精密機械,按照組件收費,這個乘法器大概需要5位到6位數(shù)人民幣。我沒有這么多錢。有朋友建議我搞FPGA,同樣,這需要學習成本。
作為學機械出身的程序員,可以理解機械工程師巧婦難為無米之炊的痛苦,但至少你得滿足其他程序員 show me the code 的訴求,不然就 cheap 了。
那就用代碼寫出來吧,這就是程序員這個職業(yè)特殊的好處,試錯成本極低,有想法馬上就能編碼。
吾嘗終日而思,不如須臾之coding也。
按照上述的機械設計,我準備將其計算邏輯用C代碼展示出來。在此之前,我們先看一下簡單的加法表計算加法的邏輯:
#include <stdio.h> #include <stdlib.h> #include <string.h>/* static char add_table[10][10] = {{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},{2, 3, 4, 5, 6, 7, 8, 9, 10, 11},{3, 4, 5, 6, 7, 8, 9, 10, 11, 12},{4, 5, 6, 7, 8, 9, 10, 11, 12, 13},{5, 6, 7, 8, 9, 10, 11, 12, 13, 14},{6, 7, 8, 9, 10, 11, 12, 13, 14, 15},{7, 8, 9, 10, 11, 12, 13, 14, 15, 16},{8, 9, 10, 11, 12, 13, 14, 15, 16, 17},{9, 10, 11, 12, 13, 14, 15, 16, 17, 18} }; */// 九九加法表 static char add_table[10][10][2] = {{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}},{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}},{{0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}},{{0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}},{{0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}},{{0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}},{{0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}},{{0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}},{{0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7}},{{0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7}, {1, 8}} };// 加法進位表,其實就是一個九九加法表的子集,考慮到加法進位最大是1,為了節(jié)省開銷,裁剪之。 static char carry_table[2][10][2] = {{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}},{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}}, };static char *register0; // 保存中間計算結(jié)果 static char *register1; // 保存最終計算結(jié)果int add(char *num1, char *num2, int len) {int i;char in1, in2, inc, carry = 0, carry_pre = 0;for (i = len - 1; i >= 0 ; i--) {in1 = num1[i] - '0';inc = in2 = num2[i] - '0';in2 = carry_table[carry][in2][1];carry_pre = carry_table[carry][inc][0];register0[i] = add_table[in1][in2][1] + '0';carry = add_table[in1][in2][0] | carry_pre;}if (carry) {memcpy(register1 + 1, register0, len);register1[0] = '1';} elsememcpy(register1, register0, len); }int main(int argc, char **argv) {char *a1, *a2, *num1, *num2;int len1, len2, len;int i;a1 = argv[1];a2 = argv[2];len1 = strlen(a1);len2 = strlen(a2);len = len1;if (len2 > len) {len = len2;}num1 = calloc(len, 1);num2 = calloc(len, 1);memset(num1, '0', len);memset(num2, '0', len);if (len1 > len2) {memcpy(num1, a1, len);memcpy(num2 + len - len2, a2, len2);} else {memcpy(num2, a2, len);memcpy(num1 + len - len1, a1, len1);}register0 = calloc(len, 1);register1 = calloc(len + 2, 1);add(num1, num2, len);printf("result:%s\n", register1); }可以試著用這個代碼做個加法看看效果。
加法表的操作實現(xiàn)了,乘法表也差不多,把九九乘法表硬編碼即可。
按照上面的機械邏輯,乘法運算就是不斷查詢乘法表和加法表的結(jié)果,當整個步驟完成后,結(jié)果也就自然而然輸出來了。下面的代碼模擬了整個過程:
#include <stdio.h> #include <stdlib.h> #include <string.h>// 九九加法表 static char add_table[10][10][2] = {{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}},{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}},{{0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}},{{0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}},{{0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}},{{0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}},{{0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}},{{0, 7}, {0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}},{{0, 8}, {0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7}},{{0, 9}, {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, {1, 5}, {1, 6}, {1, 7}, {1, 8}} };// 九九乘法表 static char mul_table[10][10][2] = {{{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}},{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}},{{0, 0}, {0, 2}, {0, 4}, {0, 6}, {0, 8}, {1, 0}, {1, 2}, {1, 4}, {1, 6}, {1, 8}},{{0, 0}, {0, 3}, {0, 6}, {0, 9}, {1, 2}, {1, 5}, {1, 8}, {2, 1}, {2, 4}, {2, 7}},{{0, 0}, {0, 4}, {0, 8}, {1, 2}, {1, 6}, {2, 0}, {2, 4}, {2, 8}, {3, 2}, {3, 6}},{{0, 0}, {0, 5}, {1, 0}, {1, 5}, {2, 0}, {2, 5}, {3, 0}, {3, 5}, {4, 0}, {4, 5}},{{0, 0}, {0, 6}, {1, 2}, {1, 8}, {2, 4}, {3, 0}, {3, 6}, {4, 2}, {4, 8}, {5, 4}},{{0, 0}, {0, 7}, {1, 4}, {2, 1}, {2, 8}, {3, 5}, {4, 2}, {4, 9}, {5, 6}, {6, 3}},{{0, 0}, {0, 8}, {1, 6}, {2, 4}, {3, 2}, {4, 0}, {4, 8}, {5, 6}, {6, 4}, {7, 2}},{{0, 0}, {0, 9}, {1, 8}, {2, 7}, {3, 6}, {4, 5}, {5, 4}, {6, 3}, {7, 2}, {8, 1}} };static char carry_table[2][10][2] = {{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}},{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}, {1, 0}}, };static char *tmp; int add(char *num1, char *num2, int len, char *result) {int i, n = len;char in1, in2, inc, carry = 0, carry_pre = 0;for (i = len - 1; i >= 0 ; i--) {in1 = num1[i] - '0';inc = in2 = num2[i] - '0';in2 = carry_table[carry][in2][1];carry_pre = carry_table[carry][inc][0];tmp[i] = add_table[in1][in2][1] + '0';carry = add_table[in1][in2][0] | carry_pre;}if (carry) {memcpy(result + 1, tmp, len);result[0] = '1';n = len + 1;} else {memcpy(result, tmp, len);}return n; }int mul(char *num1, char *num2, int len, char *result) {int i, j, n = 0, tmp_len = len;char in1, in2, res, carry = 0, carry_pre = 0;char *tmp, *sum, *mid, *mid_num1, *mid_num2, *tmp_result;tmp = calloc(len + 1, 1);mid_num1 = calloc(2*len, 1);mid_num2 = calloc(2*len, 1);sum = calloc(2*len, 1);mid = calloc(2*len, 1);tmp_result = calloc(2*len, 1);memset(sum, '0', 2*len);for (i = len - 1; i >= 0 ; i--) {in1 = num1[i] - '0';memset(mid, '0', 2*len);memset(mid_num1, '0', 2*len);memset(mid_num2, '0', 2*len);carry_pre = 0;for (j = len - 1; j >= 0 ; j--) {in2 = num2[j] - '0';res = mul_table[in1][in2][1];carry = mul_table[in1][in2][0];tmp[j] = add_table[res][carry_pre][1] + '0';carry_pre = add_table[res][carry_pre][0];carry_pre = add_table[carry][carry_pre][1];}tmp_len = len;if (carry_pre != 0) {mid[0] = carry_pre + '0';memcpy(mid + 1, tmp, tmp_len);tmp_len ++;} else {memcpy(mid, tmp, tmp_len);}tmp_len += len -1 - i;if (n > tmp_len) {memcpy(mid_num1, sum, n);memcpy(mid_num2 + n - tmp_len, mid, tmp_len);len = n;} else {memcpy(mid_num2, mid, tmp_len);memcpy(mid_num1 + tmp_len - n, sum, n);}n = add(mid_num1, mid_num2, tmp_len, tmp_result);memcpy(sum, tmp_result, n);}memcpy(result, sum, n); }int main(int argc, char **argv) {char *a1, *a2, *num1, *num2, *result;int len1, len2, len, count;int i;a1 = argv[1];a2 = argv[2];count = atoi(argv[3]);len1 = strlen(a1);len2 = strlen(a2);len = len1;if (len2 > len) {len = len2;}num1 = calloc(len, 1);num2 = calloc(len, 1);memset(num1, '0', len);memset(num2, '0', len);if (len1 > len2) {memcpy(num1, a1, len);memcpy(num2 + len - len2, a2, len2);} else {memcpy(num2, a2, len);memcpy(num1 + len - len1, a1, len1);}result = calloc(2*len, 1);tmp = calloc(2*len, 1);for (i = 0; i < count; i++) {memset(result, 0, 2*len);mul(num1, num2, len, result);}printf("result:%s\n", result); }我們用這個 “本應該用機械實現(xiàn)的軟件乘法裝置” 計算一些乘法,首先來計算幾個簡單乘法:
[root@localhost ~]# ./a.out 34 8 1 result:272 [root@localhost ~]# ./a.out 11 8 1 result:088 [root@localhost ~]# ./a.out 11 11 1 result:121 [root@localhost ~]#沒錯,符合預期。
接下來我們來計算幾個大數(shù)乘法,為了驗證結(jié)果的正確性,我們用bc程序做同樣的計算,并對比結(jié)果:
顯然,結(jié)果是正確的。
雖然O(n2)O(n^2)O(n2)的軟件性能很低,但作為一種新的乘法計算的方法,欣賞一下也可以。這就好比說,即便它真的用齒輪給湊出來了,也沒人真的會用它來做乘法運算一樣。這個裝置的效果就是, “哇!它真的可以做乘法計算,按動幾個按鈕,它真的能給出結(jié)果耶!”
你可以用小卡片代替?zhèn)鲃友b置,把輸出的 [6,3][6,3][6,3] 中的 333 拿到加法表的第三列,以此類推,最終可以得到正確的結(jié)果,嗯,這其實就是豎式計算過程的模擬。換句話說,該裝置其實是通過一系列巧妙聯(lián)動的傳動裝置,自動完成了豎式計算的過程,這些傳動組件可謂是各抱地勢,鉤心斗角,類似門電路在硅片上挖溝填壑的刻畫過程,非常巧妙。
計算機CPU運算的過程,也不過如此。
說回二進制和十進制。
現(xiàn)如今的數(shù)字系統(tǒng)普遍采用二進制,并不意味著二進制就是注定的,二進制成為數(shù)字系統(tǒng)事實的標準完全依賴地球上現(xiàn)成的一種物質(zhì),硅。如果沒有硅,實現(xiàn)數(shù)字系統(tǒng)采用二進制并非顯而易見。事實上,如果沒有硅,如果不是因為發(fā)現(xiàn)了半導體,整個數(shù)字產(chǎn)業(yè)都不一定能發(fā)展起來!
所以說, 偉大的事件是硅的發(fā)現(xiàn),而不是采用了二進制。 那些認為中國古代陰陽八卦就是二進制,所以二進制是必然之類的說法根本沒有任何憑據(jù)。
拋開硅不說,二進制表示的效能并不是最高的,最高的應該是eee進制,和eee最接近的自然數(shù)是333,也就是說三進制才是最高效的表示,其次才是二進制。三進制的高效是顯而易見的,因為宇宙萬物的任何屬性都是三元的:
- 有
- 無
- 未知
所以 三進制的三態(tài)無需任何編碼,又不會有任何浪費,就可以表示宇宙所有事物的所有屬性特征。
然而地球上找不到天然的三態(tài)物質(zhì),幸運的是地球上有硅這種二態(tài)物質(zhì),因此將第三態(tài) “未知” 用二態(tài)編碼即可,與三進制用三態(tài)3比特(三進制的比特表示大致應該是 000,111,XXX) 相比,二進制需要用二態(tài)4比特表示所有的狀態(tài),稍微有點浪費,但也還不錯。
換句話說,二進制只是eee進制,三進制的退化版本,硅的發(fā)現(xiàn)使這種退化成為可能。否則,人們普遍采用的是其它數(shù)制,和二進制的硅一樣,因為人們能夠找到其它數(shù)制的天然表示物。
由于對鐘表感興趣,特意查了一下60進制的起源。雖然我知道不可能找到正確答案,但是看看別人怎么說的也是有益的。
我倒是覺得60進制和12進制有關。下面是我上周一個早上發(fā)的朋友圈:
很多資料都說古人取60是因為要按照天象記錄時間,而錯綜復雜的天象事件周期并不一致,將周期事件畫在一個圓圈上,每一類事件都會平分這個圓,而60是1,2,3,4,5,6的公倍數(shù)。但是我覺得這個解釋有點牽強。
我倒是覺得60進制和人的手指頭腹或者指關節(jié)數(shù)量有關。按照數(shù)指頭腹的方法,一只手可以數(shù)到12,大拇指做計數(shù)指針,考慮到只有大拇指一個控制指針,只記錄倍數(shù)的話兩只手剛好可以數(shù)到60,這比數(shù)十個指頭方便有效的多,用手指進行十進制計數(shù)沒有指針,大拇指也用于計數(shù),只能靠點頭或者默念來做計數(shù)時鐘。
算卦的也采用這種數(shù)指關節(jié)的方法來計算天干地支,這個也許和計時采用60進制有關。
最大化利用手指頭上的組件最終連指腹都充分利用是60進制起源的最樸素解釋,至于說野人為了計時,需要平分一個圓找出了1,2,3,4,5,6的公倍數(shù)60,我覺得這只是一個結(jié)果,野人的所有行為都是為了活下去,沒有工夫進行歸納總結(jié),也不會主動采用迂回的方法解決問題,但是即便是野人也有雙手,利用最容易接近的東西來記錄一些事件是一種自發(fā)行為。
所以,不能用現(xiàn)代科學的觀念去評價野人的行為,否則就會犯形而上學的錯誤,這種思想是很危險的。
12進制的另一個應用是我們常用的計量單位 “一打” ,來一打雞蛋,來半打生蠔…
16進制也是古代秤桿計量上普遍使用的一個數(shù)制。
在我國古代的時候,通常都是用北斗七星、南斗六星和福祿壽三星制作秤桿,秤桿上的十六個點就分別代表著它們,這十六星在秤桿上刻制成當時的“十六星花”,這十六星告誡做買賣的人們要為人實誠,不欺瞞顧客。
摘自 http://www.sohu.com/a/339029898_587798
無論使用什么數(shù)制,都有其一定的淵源,并且都是可以做成某種自動化的裝置。這個過程可以總結(jié)如下:
利用身邊常見的現(xiàn)成物件(比如手指,指腹,指關節(jié),星座,硅這些東西)的特征確定數(shù)制,然后給予該數(shù)制一定的運算規(guī)則,利用這些物件在該規(guī)則的指導下完成一個計算步驟,比方說十進制乘法的豎式計算步驟。所謂的計算裝置,就是利用一些或機械的,或電子的構(gòu)件,將這些步驟聯(lián)動起來實現(xiàn)半自動化或者全自動化操作。
不管是機械裝置,還是電子裝置,從野人的繩子,石子,到現(xiàn)代電子計算機,直到未來的量子計算機,都是這么一個過程。
再次強調(diào),電子計算機,多虧了硅。
臨近結(jié)束,思考一個問題, 什么是計算?
發(fā)動機是計算嗎?輸入燃油和空氣,輸出動力和尾氣,和我們的輸入兩次不同位置的按力,輸出一排數(shù)字有什么本質(zhì)的不同嗎?
來自何勇的一首歌, “吃的都是良心,拉的全是思想”
跋
浙江溫州皮鞋濕,下雨進水不會胖。
總結(jié)
以上是生活随笔為你收集整理的设计一个十进制纯机械乘法器,继续大数乘法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【今日CV 计算机视觉论文速览 第13
- 下一篇: # 计算圆周长和面积