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