你覺得用不上的位運(yùn)算里,隱藏著 CPU 實(shí)現(xiàn)的秘密
本文轉(zhuǎn)載自微信公眾號(hào)「神光的編程秘籍」,作者神說要有光zxg 。轉(zhuǎn)載本文請(qǐng)聯(lián)系神光的編程秘籍公眾號(hào)。
我們學(xué) JS 的時(shí)候都會(huì)了解下位運(yùn)算,在 React、Typescript 等源碼中也頻繁見到位運(yùn)算的蹤影,但在業(yè)務(wù)代碼中從來不會(huì)這么寫,它好像離我們很遙遠(yuǎn)。
但是位運(yùn)算真的遙遠(yuǎn)么?
其實(shí)并不是。你寫的所有代碼最終都會(huì)轉(zhuǎn)為位運(yùn)算,位運(yùn)算里隱藏著 CPU 實(shí)現(xiàn)的秘密。
下面我們就來談一下位運(yùn)算與 CPU 的關(guān)系以及位運(yùn)算在代碼中的應(yīng)用。
從晶體管造 CPU
晶體管
先來了解下晶體管。
下面這個(gè)是三極管,它就是一種晶體管,特性是從一端通電,另外兩端的電流就可以通過,不通電,另外兩端的電流就不能通過。
這特性不就是一個(gè)開關(guān)么?
它和下面這個(gè)東西差不多,只不過不需要手動(dòng)開關(guān),而是通過通電來控制開關(guān)。
開和關(guān),就是 1 和 0。計(jì)算機(jī)世界的組成單元 1 和 0 就是這么實(shí)現(xiàn)的。
邏輯電路
有了 01 就可以構(gòu)成邏輯電路了,也就是與門、或門、非門、異或門等構(gòu)成的電路。
它們的電路符號(hào)是這樣的:
與門:
或門:
非門:
異或門:
它們?cè)?JS 中怎么表示呢?
與 &
或 |
非 ~
異或 ^
它們就是通過三極管構(gòu)成的邏輯電路的基本單元,能夠?qū)崿F(xiàn)基于 0 1 的邏輯。
什么邏輯呢?
1 & 0 為 0
1 | 0 為 1
~1 為 0
0 ^ 0 為 0
與或非大家比較熟悉了,這里講一下異或:
異或是相同為 0,不相同為 1,也就是一個(gè) 0 和一個(gè) 1 才會(huì)得出 1,否則為 0
運(yùn)算器
我們了解了位運(yùn)算是什么,邏輯電路是什么,那有了邏輯電路能做什么呢?
能做的可多了,CPU 不就是一個(gè)大邏輯電路么,它就是建立在位運(yùn)算基礎(chǔ)上的。
比如我們實(shí)現(xiàn)下 ALU 運(yùn)算器:
首先實(shí)現(xiàn)加法:
加法在二進(jìn)制里面就是異或,不信我們來試一下:
1 和 0, 0 和 1 想加是 1 ,而 0 和 0,1 和 1 相加都為 0,這不就是異或么。
當(dāng)然,還要處理進(jìn)位,進(jìn)位可以通過與運(yùn)算得到,比如上面那四個(gè),只有 1 和 1 相與才為 1,否則都為0,這就算出了進(jìn)位。
能夠按位相加和進(jìn)位,就能實(shí)現(xiàn)加法器。
- function add(a, b) {
- let sum = a;
- let carry = b;
- let temp;
- while(carry != 0) {
- temp = sum;
- sum = temp ^ carry;
- carry = (temp & carry) << 1;
- }
- return sum;
- }
測(cè)試一下加法器:
注意,上面的與和異或不都有邏輯電路么,那用電路實(shí)現(xiàn)上面這段代碼不就是硬件實(shí)現(xiàn)的加法器么?
有了加法,很容易就可以得到減法器:
- function subtract(a, b) {
- var substrahend= add(~b, 1)
- var sub= add(a, substrahend)
- return sub
- }
把被減數(shù)變?yōu)橄喾磾?shù),再和被減數(shù)相加不就是減法么?
至于這里為什么是取反加一,就涉及到原碼反碼補(bǔ)碼了,
因?yàn)?1 對(duì)應(yīng) -1,而 0 呢?并沒有 -0,所以就少了一位。所以要加一才能對(duì)上,也就是補(bǔ)碼的“補(bǔ)”的意思,補(bǔ)上沒有 -0 導(dǎo)致的缺少的那個(gè)編碼。
實(shí)現(xiàn)了加法器、減法器之后,乘法和除法也就有了,因?yàn)槌朔ú痪褪嵌鄠€(gè)加么?除法不就是多個(gè)減么?
就這樣,我們從位運(yùn)算實(shí)現(xiàn)了加減乘除。
對(duì)應(yīng)到硬件上呢?就是我們通過三極管實(shí)現(xiàn)了邏輯電路,然后又用邏輯電路實(shí)現(xiàn)了加減乘除。
CPU
上面那個(gè)東西在 CPU 里叫做 ALU,算術(shù)邏輯單元,可以做邏輯運(yùn)算、算術(shù)運(yùn)算。
而且基于三極管還可以做到存儲(chǔ) 0 1 等目的,構(gòu)成寄存器。
有了這些東西我們就可以實(shí)現(xiàn)一個(gè) CPU。
神不神奇,通過晶體管和位運(yùn)算,我們就能把 CPU 造出來,雖然也就需要數(shù)億個(gè)晶體管吧。
當(dāng)然,我們只了解這些意義不大,還要了解位運(yùn)算的應(yīng)用。
位運(yùn)算的應(yīng)用
文件系統(tǒng)
看過我前面一篇文件系統(tǒng)實(shí)現(xiàn)原理文章的小伙伴會(huì)知道,硬盤會(huì)劃分為數(shù)據(jù)塊來使用,一個(gè)文件就是由多個(gè)數(shù)據(jù)塊構(gòu)成的:
文件會(huì)通過一個(gè)叫 inode 的結(jié)構(gòu)來記錄用到了哪幾個(gè)數(shù)據(jù)塊:
那我想存文件的時(shí)候,怎么知道哪些塊可用呢?
一個(gè)塊只有空閑和非空閑兩種狀態(tài),一個(gè)位 0 和 1 就可以保存,所以會(huì)用位圖這種結(jié)構(gòu)來記錄。
比如當(dāng)我存儲(chǔ)圖片占用了 2 和 4 號(hào)塊的時(shí)候,就會(huì)把位圖的對(duì)應(yīng)位置置為 1,否則為 0。那么當(dāng)我需要存儲(chǔ)文件的時(shí)候,從位圖中查一下就知道哪些數(shù)據(jù)塊可用了。
通過位圖記錄狀態(tài),通過位運(yùn)算來判斷狀態(tài)。占用空間小,運(yùn)算效率高。
操作系統(tǒng)級(jí)別都是這么干的,很多追求性能的庫(kù)也都這么干。
React 和 Typescript
在 React 和 Typescript 等源碼中,經(jīng)常會(huì)見到一個(gè)叫 flags 的東西,flags 就和我上面說的位圖差不多,通過一個(gè)位標(biāo)識(shí)一個(gè)狀態(tài)。
比如下面這段 React 的源碼:
這就是判斷了 這個(gè) fiberNode 是否有 Placement 或者 Hydrating 的狀態(tài),如果有就 xxx。
再來看 Typescript 的源碼中的這段代碼:
~ 是取相反狀態(tài),再 & 就是判斷是否沒有這個(gè)狀態(tài),然后通過 | 設(shè)置到 flags 里面,意思就是這個(gè) flags 加上沒有 xx 狀態(tài)的標(biāo)識(shí)。
業(yè)務(wù)代碼
操作系統(tǒng)、優(yōu)秀的庫(kù)中都用到了位運(yùn)算,因?yàn)樗鼈冃阅芨撸苯佑秒娐匪?,存?chǔ)空間也小,那是不是我們代碼中可以用呢?
可以是可以,但是業(yè)務(wù)代碼追求的更多是可維護(hù)性,如果你寫出上面那種 typescript 的源碼那樣的位運(yùn)算,后面接手的人不想打死你才怪。
總結(jié)
CPU 通過晶體管實(shí)現(xiàn)了電路的開關(guān),也就是 0 和 1,然后組成了與或非異或門,進(jìn)一步構(gòu)成邏輯電路,邏輯電路可以實(shí)現(xiàn)加減乘除,構(gòu)成 ALU,加上寄存器等部件就構(gòu)成了 CPU。
所以位運(yùn)算是直接用電路算,效率最高,其他的運(yùn)算最終也會(huì)轉(zhuǎn)為位運(yùn)算。
操作系統(tǒng)文件系統(tǒng)的設(shè)計(jì)就用到了位圖和位運(yùn)算,React 和 Typescript 源碼中也大量用到 flags。但這不意味著業(yè)務(wù)代碼就可以用,因?yàn)闃I(yè)務(wù)代碼還是可維護(hù)性更重要一些。
位運(yùn)算里面隱藏著 CPU 實(shí)現(xiàn)的秘密,并不只是一個(gè)炫技的手段那么簡(jiǎn)單。