我们学 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,这就算出了进位。
能够按位相加和进位,就能实现加法器。
代码语言:javascript复制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;
}
测试一下加法器:
注意,上面的与和异或不都有逻辑电路么,那用电路实现上面这段代码不就是硬件实现的加法器么?
有了加法,很容易就可以得到减法器:
代码语言:javascript复制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 实现的秘密,并不只是一个炫技的手段那么简单。