身为程序员多年, 作者今天突然对这件事感到十分好奇了. 我问计算机芸芸部件, 1+1 究竟是如何计算的, 他们都茫然的看着我.
打开谷歌浏览器 ->Console 面板, 大脑向双手不停发送生物电信号, 肌肉细胞大量钙分子游离到细胞外肌肉拉紧, 钙分子回到细胞内肌肉又松开, 钙分子来来回回进进出出多次, 在大脑总枢纽指挥下, 在多指配合下, 最终完成了 1,Shift+,1 的键入, 然后回车, 输出如下:
谷歌浏览器返回了 2.
作者问浏览器:"你小子是怎么知道 1+1 等于 2 的? 纵观人类进化史, 从学会使用石头, 到学会结绳记数, 用了 100 万年. 你年纪轻轻 28 岁, 是怎么知道 1+1 等于 2 的?"
浏览器说:"我不知道啊, 是 v8 告诉我的."
- "v8 是谁? 是男是女?"
- "非男非女, 亦男亦女. v8 是谷歌研发的 JavaScript 引擎, 你发给我的 JS 代码, 都是由他执行的."
- "把 v8 叫来, 我有事问他."
不一会儿, v8 来到我面前. 我问他:"你是怎么知道 1+1 等于 2 的? 人类世界上最聪明的孩子降生时, 都不知道 1+1 是等于 2 的. 你是怎么知道的?"
"我并不知道 1+1 等于几, 我所有结果都是基于您的输入给出的."
作者问 v8:"当浏览器把 1+1 发给你以后, 你干了啥?"
"我就是进行了词法分析, 语法分析, 建立了语法树, 符号表等..."
浏览器说:"这些都不要说了, 这都是身为编译器 / 解释器的份内之事, 大家都是这么干的, 我解析 html 标签也是这么干的. 直接说你解析完了干了什么?"
v8 道:"解析完了, 我就使用了 MacroAssembler 库..."
浏览器说:"MacroAssembler 库就是缩写为 masm 的汇编库吧, 我去年在 Strongtalk VM 那里见过他, 要知道 Strongtalk VM 可是大大鼎鼎的 Java 虚拟机 HotSpot JVM 的前辈呢! Java 可是比 JS 快多了!" 浏览器显得是一个见多识广的人. 要知道全世界的 www 网页都展示都在他上面显示, 他真的是见多识广.
但作者不喜欢浏览器自作聪明,"浏览器别打岔, v8 你继续讲, 使用 masm 干了什么?"
v8 道:"masm 提供了很多方法, 基本和 JS 是一一对应的, JS 语句是什么, 就调用对应的 masm 方法. 例如 1+1 这名 JS 代码, 对应调用 masm 的 C++ 代码是这样的:
- #define __ masm.
- __ mov(eax, 1) // 在这里 __ 是一个宏, 在预处理之后将被统一替换为 "masm.". 这一句是将寄存器 eax 设置为 1
- __ add(eax, 1) // 这一句将寄存器的值加 1
- __ ret(eax) // 这里返回寄存值的值
- (以上只是示例, 伪代码不要当真)
上面是 C++ 代码, 在内存里生成机器码大概长这个样子:"
- B8 01 00 00 00 ;mov eax,1
- 83 C0 01 ;add eax,1
浏览器道:"胡说! 机器码都是二进制格式 010110010 这种的, 你这种 B8,C0 是什么机器码?"
v8 道:"我没有胡说啊!'B8 01 00 00 00'这是二进制机器码的 16 进制展示, 人类使用 16 进制更方便阅读, 但我和 CPU 交流都是以 010110010 这种二进制方式."
这时 CPU 听到有人叫他的名字, 按耐不住了. CPU 问:"谁在说俺的大名! v8, 那后面的'mov eax,1'是什么? 我怎么从来没见你提过?"
v8 道:"'mov eax,1'是机器码注释, 是给人类大哥看的, 我给你看的都是二进制字节码, 是 010110010 这种. 像 mov 它只是诸如 1010 这种汇编指令的代名词, 人类写的是 mov, 汇编编译器译完就是 1010 了.
eax 是寄存器地址,'mov eax,1'这句指令就是将寄存器的值设为 1. 同时, 它下面那句'add eax,1'是将寄存器的数值加 1.add 与 mov 不就是你的两个指令吗, CPU 大哥? 如果我发错了指令, 大哥从来都不曾理会我."
CPU 点点头.
浏览器继续问:"好啊 v8, 用户每天都骂我慢得像蜗牛, 罪魁祸首原来在你这. 码农都说你快, 我每天看你却很慢. 原来你是将 js 代码先转成了汇编代码, 再将汇编代码转成为机器器, 一件事转二道手续, 这样能不慢吗? 为什么不直接将 js 代码转为二进制机器码交给 CPU 大哥执行?"
"哈哈哈",v8 大笑道:"浏览器, 你只知表面, 不知就理. js 是解析型语言, 如何直接编译成机器码? 如果是这样, 它不就和 Java 一样, 是编译型语言了吗?"
浏览器反驳道:"虽然是解释型语言, 为什么不能先编译再执行? 在 Java 版 JS 解释器 rhino 中, js 脚本不是被编译为 Java 字节码执行的吗?"
作者觉得讨论有点跑偏了, 道:"言归正传. v8, 浏览器给你的 js 代码, 你是读一行调用 masm 转化一行, 还是读完了一起调用 masm 再转化的?"
v8 说:"是一起转化的, 但这一切都是在内存那里折腾的. 我有两个助手, 一个叫初级全码编译器 (官名叫 Full Code Generator), 他将所有 js 代码依次调用 masm 全部在内存中走了一遍; 另一个叫优化能手编译器 (官名叫 Crankshaft), 他针对运行多次的代码, 以全码编译器的编译结果为基础, 再作一次优化编译, 目的是使代码执行更快."
这时内存说话了,"是啊, 每次都把我折腾的晕头转向. 别的 exe 文件, 是先于我这里加载, 后交给 CPU 运行, 一次搞定, 很干脆. 唯有 v8 交给我的执行文件, 连个名字都没有, 忽长忽短, 变化莫测."
CPU 说:"但是我感觉 v8 交给我的机器码和普通的 exe 文件机器码没有什么区别, 在我这里他们都是合法公民. 只要机器指令全部正确, 我就能返回正确的运行结果."
看来 v8 并不知道 1+1 为什么等于 2,v8 为了执行 JS 快一点, 大量占用了内存空间, 是用 "空间换时间" 的方法, 博得了 "v8 引擎执行快" 的美名. 具体为什么 1+1 等于 2, 还需要问问 CPU.
作者对 CPU 道:"CPU, 你说只要指令正确, 你就能返回正确结果. 那么 v8 将 1+1 的机器码传给你, 你都做了什么?"
CPU 道:"报告主人, 我什么都没有做. 我做的一切, 都是让按照您的指令完成的. 这一切都是您的智慧啊!"CPU 态度很诚恳.
浏览器小声道:"嘘, 马屁精!"
CPU 不理会, 继续说道:"首先, 当我看到'mov eax 1', 就知道这是叫我将值 1 移动到寄存器 eax 处. 我有一个助理, 叫指令指挥官, 他负责指令的分类与调度. 例如他看到指令是 010100010010, 首先从前 4 位 0101 判断, 这是一个寄存器设置命令, 于是就打电话通知寄存器老头来领取数据包裹; 如果看到前 4 位是 1010, 就知道这是一个加法指令, 就打电话通知算术运算单位的加法器来领取数据和任务, 待加法器计算完了, 他会将运算结果发给寄存器老头保存."
这时浏览器对 CPU 如何计算的也起了好奇, 问道:"不要说人话, 讲机器语言, 说人话我们听不懂. 指令指挥官是如何给你的单位职员分派任务的? 他看到 0101, 是怎么知道应该分派给寄存器老头的?"
"这么简单你都不明白吗? 比如 0101 这 4 个 bit, 依次代表 4 个路口, 每个路口有两个岔, 0 向左转, 1 向右转, 这样 0101 一路走下来不就知道是哪个职员负责了."
指令分派确实简单, 关键还在加法器上. 1+1 等于几是他算出来的, 于是作者问道:"CPU, 那加法器是如何计算 1+1 的呢?"
CPU 道:" 这就不那么简单了. 加法器并不知道 1+1 等于几. 加法器是由半加器组成的, 而半加器又是由一个异或门加一个与门组成的, 如下所示是一个半加器:
(在上图中, A,B 是输入, S 是结果, C 是进位结果.)
学过数学很容易理解, 异或门的逻辑是这样的:
负负得负, 正负得正, 负正得正, 负负得正, 这就是异或门逻辑. 如果说异或门电路有点复杂, 那么异或门又可以由与非门表示:
(读者可以将 1,0 不同值分别代入 A,B, 验证异或门结果 Q)
与非门的逻辑是这样的:
负负得正, 负正得正, 正负得正, 正正得负. 与非门简单电路可以是这样的:
x,y 是两个开关. x,y 的开状态为 1, 关状态为 0.x,y 相当于与非门中的 A,B.x,y 状态全开, 以及任何一个状态为开, 电路都是不通的. 只有当 x,y 状态全为关, 电路才是通的.
与非门可以由开关设计组成, 异或门也可以由开关组成. 异或门加一个与门组成了半加器, 多个半加器串到一起, 就组成了全加器, 如下所示:
低位半加器的进位结果恰是高位半加器的输入, 合在一起就组成了一个多位全加器. 所以, 我的加法运算能力也不是无限的, 能算多大数字是由硬件决定的."
这下明白了, CPU 并不知道 1+1 等于 2. 之所以 1+1 能算出等于 2, 是人类在设计 CPU 的时候赋能给它的. 而 CPU 内所有的运算, 归根结底又都是开关的开合. 从这点来看, 计算机的鼻祖竟然是小小的开关.
浏览器问:"CPU, 这样说来你的加法器都是由众多开关实现的. 那减法运算, 乘法运算, 除法运算又是怎么实现的?"
CPU 道:"减法在我这里也是加法, 乘法是换算为多位加法累加的, 除法又可以换算为乘法. 所以, 所有四则运算都是由加法实现的. 包括文字与音频, 视频信息处理, 在我这里都是二进制的加减乘法与逻辑与非."
浏览器又问:"那这样说, 在你内部肯家有很多很多的开关喽?"
CPU 说:"人类发明了一种双极型三极管, 简称晶体管. 在我内部, 晶体管不多, 也就有几十亿个吧. 每个晶体管就相当于一个电路中的开关."
原来作者在浏览器里简单敲一个 1+1,CPU 那里就要噼里啪啦开关个不停.
计算机并没有智能. 我们从宏观上看, 仿佛计算机拥有了智能一般, 能处理很多复杂的问题, 其实都是通过数以亿计的晶体管开关电路实现的, 并且这种能力也都是人类赋予它的.
在人的大脑中, 也有几十亿个神经元, 像一个计算机一样. 人为什么拥有智能? 或者人根本也并不拥有智能, 在上帝那里, 我们的大脑也只是按照他老人家的设计表现开头状态而已?
2018 年 12 月 21 日于北京
艺览无余
首发于 "艺述思维" 微信公众号: JS 是如何计算 1+1=2 的? https://mp.weixin.qq.com/s/vZepd_FcPL4Oh-UmhZ42fg
来源: http://www.jianshu.com/p/77a1494489d4