JavaScriptCore, WebKit的JS实现(一)

目前,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,虚拟机,webkit,编译器,mozilla)