身为程序员多年,作者今天突然对这件事感到十分好奇了。我问计算机芸芸部件,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说:“人类发明了一种双极型三极管,简称晶体管。在我内部,晶体管不多,也就有几十亿个吧。每个晶体管就相当于一个电路中的开关。”(注:现代CPU里用的是更高级最小的场效应管)
原来作者在浏览器里简单敲一个1+1,CPU那里就要噼里啪啦开关个不停。
计算机并没有智能。我们从宏观上看,仿佛计算机拥有了智能一般,能处理很多复杂的问题,其实都是通过数以亿计的晶体管开关电路实现的,并且这种能力也都是人类赋予它的。
在人的大脑中,也有几十亿个神经元,像一个计算机一样。人为什么拥有智能?或者人根本也并不拥有智能,在上帝那里,我们的大脑也只是按照他老人家的设计表现开关状态而已?
2018年12月21日于北京
首发于“艺述思维”微信公众号:JS是如何计算 1+1=2 的?