JavaScriptCore, WebKit的JS实现(完)

简介: 目前,JavaScript的实现已经成为编译领域的一个重要组成部分。主要包括Google的V8, Mozilla的SpiderMonkey, 还有Webkit使用的JavaScriptCore。

目前,JavaScript的实现已经成为编译领域的一个重要组成部分。主要包括Google的V8, Mozilla的SpiderMonkey, 还有Webkit使用的JavaScriptCore。

 

jsc:WebKit的JS引擎( js for webkit)

JavaScriptCore (JSC)正是WebKit的JavaScript实现。

 

起初,JavaScriptCore是一个基于的树的简单解释器(tree-based interpreter). 但在2008年6月,几位Apple的牛人为JSC重新写一个编译器(compiler)和一个字节码解释器(bytecode interpreter),将原先的实现抛弃了, 这个新的实现被称为SquirrelFish(金鳞鱼). 在苹果内部的产品代号是"Nitro"。

 

JSC的字节码解释器(bytecode interpreter)很棒,令人着迷. 我将在下面说说更多的细节。

 

2008年后, WebKit的伙计们新增了Inline caches, 一个基于正则表达式(regular expression)的JIT, 和一个简单方法(simple method)JIT, 随后新的版本被称为SquirrelFish Extreme(Nitro Extreme),可以简称为SFX。正式的名称仍然是JavaScriptCore。

 

JSC的伙计们做得很棒,以至于Mozilla SpiderMondkey的骇客们(hackers)也直接采用了JSC的基于正则表达的JIT(regexp JIT)和原生汇编程序(native-code assembler)。

 

对于JSC的2009年和2010年都花在了它的巩固(consolidation)上了. JSC既有一个JIT,又有一个字节码解释器(bytecode interpreter), 他们想要同时维护它们,所以为他们的协作,需要做大量的重构和调整。在这阶段SFX在x86架构上得以加强,同时也增加了ARM和其它架构下的实现。

 

但随着2010后期V8 Crankshaft(曲轴)的发布, JSC性能又变得不足了。JSC的那些人又开始开发他们称为的DFG JIT (data-flow graph JIT), 使JSC更接近于Crankshaft.

 

JSC可以由三个引擎组成: 解释器(interpreter), 简单方法JIT(simple method JIT), 和DFG JIT. 三种形式有一个层次化的编译过程:初始的解析和编译生成字节码(byte-code), 再由simple method JIT加以优化, 最后再由DFG JIT加以优化。在实践中, 多数平台下并没有解释器,所有的代码都是通过method JIT运行。DFG JIT随着Mac OS X Lions的Safari浏览器一起发布,但并没有在除了64-bit Mac OS以外的系统上使用。

 

寄存器式的虚拟机(a register vm)

以下register VM或register machine都是指基于寄存器的虚拟机或寄存器式虚拟机,而stack machine或stack VM都是指基于堆栈的虚拟机或堆栈式虚拟机。网上有专门的论文讨论两者的细节。

解释器有很多有趣的东西,但重要的是字节码的定义。字节码实际上是JSC的高层次的中间表示(intermediate representation, IR)。

在V8中,高层次的中间表示是JS代码本身。当V8第一次看到一段代码,它先预解析(pre-parses)并报告语法错误。然后当它需要分析源代码,无论是在full-codegen编译器或在Hydrogen中,它都重新解析代码为AST(抽象语法树,abstract syntax tree),然后基于AST运行。

相比之下,JSC首先就将代码完全解析为AST,然后再将AST编译为字节码。这时源代码就不再需要了,所以会被抛弃。解释器直接从字节码解释。简单方法JIT编译器也是直接编译字节码。在进一步优化和生成原生代码前,DFG JIT必须重新将字节码转成SSA(static single assignment)风格的中间表示(IR)。这个过程开销较大,但对于频繁使用的hot code是值得的。

正如您可以看到的,字节码是所有JSC引擎的通用语言,所以了解它很重要。

真正进入正题之前,我要说一个关于术语的题外话。以我的经验,一个虚拟机传统上被认为是一个解释虚拟指令序列的软件。相对而言,实体机是在硬件上解释机器码或原生指令序列。

 

最近这些事变得更复杂了。几年前,一个常见的问题是“JavaScript是解释类语言还编译类语言?“ 这个问题其实很奇怪,因为“解释”或“编译”是属性,而不是语言的实现。再说了,实现可以是编译为字节码,然后解释那些字节码, JSC就是这样做的。

 

但是最后,如果将所有代码编译为字节码, 那虚拟机的意义又体现在哪里呢?即使V8已经没有解释器了, 但V8的骇客们仍然自称为"虚拟机工程师(virtual machine engineers)"。(ARM的模拟器不算在内的话,qemu下运行的程序又如何呢?).

 

总之,仍然可以称JSC的高层次的中间表示是基于寄存器的虚拟机,并有一系列的虚拟指令集,就像是解释器和简单方法JIT实现的那样。

 

"基于寄存器的虚拟机(register machine)", 是相对"基于堆栈的虚拟机(stack machine)"而言的. 它们间的差异主要是前者中所有的临时变量都有名字且存储在一组stack frame中。而后者临时结果被压入堆栈,并且绝大多数指令都是从堆栈中弹出它们的操作符。

 

(顺便提一下, V8的full-codegen编译器是使用类似stack-machine的方式执行AST的。 V8中有不少Bug都是来源于从full-codegen到Crankshaft转换时使用的堆栈状态模型(accurately modelling the state of the stack)。)

 

对于一个解释器来说,我相信基于寄存器的虚拟机才是正确的方式。这里我要说明一些理由。

 

首先,stack machines不利于临时变量的命名。例如下面的代码(Lisp):

 
(lambda (x)
  (* (+ x 2)
     (+ x 2)))
 

我们可以删除公共表达式(common sub-expression elimation)来优化它:

 
(lambda (x)
  (let ((y (+ x 2)))
    (* y y)))
 

以stack machine能否胜出呢? 考虑上面第一段代码的指令序列::

 
; stack machine, 未优化
0: local-ref 0      ; x
1: make-int8 2
2: add
3: local-ref 0      ; x
4: make-int8 2
5: add
6: mul
7: return
 

上面第二段代码的指令序列:

 
; stack machine, optimized
0: local-ref 0      ; push x
1: make-int8 2      ; push 2
2: add              ; pop x and 2, add, and push sum
3: local-set 1      ; pop and set y
4: local-ref 1      ; push y
5: local-ref 1      ; push y
6: mul              ; pop y and y, multiply, and push product
7: return           ; pop and return

两种方式下并没有什么改善, 因为存储到本地变量以及将它们压回堆栈使用是不同的指令集,并且一个过程的时间开销同它所执行的指令数之间是一个线性关系。

 

而在register machine中,事情变得简单多了,而CSE最终获胜:

 
0: add 1 0 0           ; add x to x and store in y
1: mul 2 1 1           ; multiply y and y and store in z
2: return 2            ; return z
 

在register machine中,变量命令没有任何妨碍。使用register machine可以减少push/pop的干扰,而专注于要做的事。

 

并且因为在指令中包含了操作符的名称(或者说是位置),register machine可以使用更少的指令来完成同一件工作。这减少了调度成本。

 

此外,register VM中调用帧(call frame)的大小在调用前都是可知的, 这样你就可以在压入数据时检查以避免溢出。(一些stack machine也有这个属性,比如JVM)。

 

选择register machine的最大的优势是你可以从传统编译器优化中获益,如CSE和寄存器分配(register allocation)。在上面的例子中,我们使用了三个虚拟寄存器, 但在现实中我们只需要一个。生成的代码也更接近真正的机器码, 因此很容易用于JIT。

 

不利的一面是, register machine通常占用更多的内存。JSC有一个特别的情况,操作码(opcode)和每个操作数(operand)占满了整个机器字。这样做是为了实现“direct threading”, 这个操作码不是在跳转表(jump tables)中索引, 而实际上是相应地址的标签。这在JS不会将字节码序列化到外部磁盘的情况下还免强可以接受。但对于其它需要的重定位的情况就可能会造成丢失。这个功能默认是关闭的。

 

解释器的stack frame包含一个6字节的帧(six-word frame), 参数,最后是局部变量。一个程序调用会预留一个stack frame的空间,然后将参数压入堆栈 (或者说将它们设置到stack frame中n + 6位置的寄存器中)——然后调整帧的指针。JSC中的堆栈被称为“寄存器文件(register file)”, 帧指针(frame pointer)则被称为“寄存器窗口(register window)”。这些名字和堆栈世界(stack world)里的“激活记录(activation records)”一样难以理解。

 

原文地址:Wingolog

上一篇: JavaScriptCore, WebKit的JS实现(完)

转载请注明出处:http://blog.csdn.net/horkychen

目录
相关文章
|
JavaScript 前端开发
javascript深拷贝和浅拷贝以及实现方法(推荐)
javascript深拷贝和浅拷贝以及实现方法(推荐)
532 0
javascript深拷贝和浅拷贝以及实现方法(推荐)
|
JavaScript 前端开发
利用JavaScript实现二级联动
利用JavaScript实现二级联动 要实现JavaScript二级联动效果,首先要确定需要哪些技术: 二维数组 for in循环 new Option(text,value,true,true) add(option,null) onchange() 表单事件 HTML代码: <!-- <input type="text" id="text"> --> 请选择省份: <select name="" id="provinces"> <!-- <option value="江苏省">江苏省</option>
|
JavaScript 前端开发
JavaScript函数柯里化的实现原理,进来教你完成一个自己的自动实现柯里化方法
JavaScript函数柯里化的实现原理,进来教你完成一个自己的自动实现柯里化方法
167 0
|
移动开发 JavaScript weex
weex-自定义module,实现weex在iOS的本地化,js之间互相跳转,交互,传值(iOS接入weex的最佳方式)
weex-自定义module,实现weex在iOS的本地化,js之间互相跳转,交互,传值(iOS接入weex的最佳方式)
219 0
|
JavaScript
JS中实现或退出全屏
JS中实现或退出全屏
153 0
|
前端开发 JavaScript
前端:JS实现双击table单元格变为可编辑状态
前端:JS实现双击table单元格变为可编辑状态
365 0
|
JavaScript 算法 前端开发
【前端算法】JS实现数字千分位格式化
JS实现数字千分位格式化的几种思路,以及它们之间的性能比较
276 1
|
算法 前端开发 JavaScript
【前端算法】用JS实现快速排序
理解数组方法里面运用到的算法,splice 和 slice的区别
113 0
|
JavaScript 前端开发 算法
【前端算法】javaScript实现二分查找
如何使用JS实现一个合格的二分查找
191 0