引擎浅谈 SpiderMonkey & Google V8

SpiderMonkey

代表产品: ForeFox 浏览器

网站: https://developer.mozilla.org/en-US/docs/SpiderMonkey

博客: https://blog.mozilla.org/javascript/

代码: http://hg.mozilla.org/mozilla-central/file/tip/js/src

猴子算是最早问世的 js 引擎了, 第一个版本由布兰登·艾克设计(他于10天内开发出JavaScript), 后交给 Mozilla 维护, SpiderMonkey 只是一个项目代号, 经过长时间的迭代完善,先后出现了 TraceMonkey , JägerMonkey ,IonMonkey 等 JIT 编译器.(这3 只是SpiderMonkey的JIT编译器. 想弄清他们的关系 可以看看大牛Leary的博客,很清楚 ) 后来 Mozilla 率先提出 webAssbly(网页汇编)概念 , 并采用了 OdinMonkey 来加速 asm.js.

这年头,一个脚本要是不带JIT .还真不还意思说自己是出来混的. 但是为脚本开发一个JIT引擎却不是一个小工程 ,脚本本身功能越多,需要支持的平台越多, JIT的复杂度就会越大.( 参见 知乎: Python离JIT还有多远? ) 较早出现JIT的是Lua http://luajit.org/ , 这可能是由于 Lua 从诞生起就被定义为是嵌入式脚本,体积功能都非常小.JIT 的开发也会比较容易, 而 js 无论从功能还是体系上, 都比 Lua 复杂太多.

SpiderMonkey 的特点:

语言: c/c++ 混合编译.

早期的代码是使用C完成的,后来加入JIT后逐渐使用C++

GC 垃圾回收: 分代式GC.

运行: 采用解释/JIT 混合方式,JIT采用多层编译模式. (不同于V8 这种全靠编译执行的引擎. SpiderMonkey 同时兼顾了2种执行方式 )

这里可能要费些笔墨了,首先说什么是解释执行..

解释执行.

SpiderMonkey 首先将 js 文件转换成抽象语法树(AST)再转换成字节码文件 (这一步与java等大多数语言类似), 这种字节码文件可以一行行的丢给解释引擎来执行,我们只需要根据不同 platform 开发出不同的解释引擎就可以了. 本质来讲, SpiderMonkey 就是一个虚拟机 VM. 解释引擎消化字节码的效率就是整个js文件被执行效率. 那我们为什么不直接把 AST 丢给解释器呢 ? 原因是字节码比 AST 有更好的内存布局,曾经有人做过实验,同样的逻辑使用字节码比使用 AST 效率高3倍以上,当然这紧紧是在解释执行层面上的结论. 对于编译执行的方式,直接使用 AST 也未尝不可.(参考 R大的博文 为什么大多数解释器都将AST转化成字节码再用虚拟机执行,而不是直接解释AST? )

ps: 可以使用 SpiderMonkey 自带的jsshell工具,来查看生成出来的字节码.

下面就是一段 SpiderMonkey 的字节码

— SCRIPT tests/js1_8_5/shell.js:1 —
00000: 10 getgname “version”
{“interp”: 1}
00005: 10 typeof
{“interp”: 1}
00006: 10 string “undefined”
{“interp”: 1}
00011: 10 ne
{"interp": 1}
— END SCRIPT tests/js1_8_5/shell.js:1 —

JIT 即时编译执行.

这里总要有人提性能的梗, 从理论上来说,直接编译成平台相关机器码,执行速度确实会快一些.但是这也要看优化程度. 一个欠优化的 JIT 编出的程序未必一定比解释执行来的快. 而且编译时用户需要等待.这段时间怎么补偿? 所以 SpiderMonkey 两种方式都保留.

SpiderMonkey 有很多 JIT, 较早的 TraceMonkey 发布于 2008年8月23日 , 被用作 Firefox 3.5 的 JIT引擎, 后来又出现了 JägerMonkey 大幅改善了 TraceMonkey 的性能 ,但是似乎还是缺少一个 "快速编译JIT" 来在执行性能,和 编译耗时之间做平衡. 直到 Baseline 编译器 出现

javaScript代码在SpiderMonkey中的执行流程:

  1. js源码 => 抽象语法树AST => 字节码 然后被解释执行, 同时开始收集类型信息,为后续的 JIT阶段做准备.
  2. 经过运行一段时间后,发现某段代码调用次数很多(hot code). 便启动 Baseline 开始快速编译这一段代码, 时间有限,编译出的代码或许不是最优的. 这里 时间 > 性能
  3. 再经过一段时间后,发现某段代码调用次数仍然很多(really hot code),便启动 IonMonkey ,花大力气 精细编译这部分代码, 这里 性能 > 时间

这也就是多层 JIT 编译模式..

Google V8

代表产品: Chrome Nodejs

网站: http://code.google.com/p/v8

博客: http://blog.chromium.org/search/label/v8

代码: https://code.google.com/p/v8/wiki/Source?tm=4

先上一个传送门 How the V8 engine works?

语言: c++.

GC 垃圾回收: 分代式GC.

运行: 纯 JIT 多层编译方式.

V8小组里都是一群屌爆了的专家。其核心人物 Lars Bak 更是 HotSpot(Java JIT) 的作者 . V8 目前是 chrome 的 js 引擎, 有趣的是 safari 和 chrome 采用了相同的渲染引擎---webkit , 但没有采用相同的 js 引擎.

V8 和 SpiderMonkey 主要区别是 它彻底抛弃了字节码, 所以 V8 并没有解释引擎. 全部依赖高性能JIT. 但由于 JIT 在 IOS 中被禁用,IOS版的 Chrome 无法使用 V8 这种引擎. 所以干脆直接用了 WebView , 到是苹果自家的 safari 可以使用 JIT.

至于对象属性管理,隐藏类(Hidden Class)等手段在 SpiderMonkey 中也能找到相似的技术.这里稍稍提一下.

隐藏类(Hidden Class):

其实隐藏类并没有官方文档上说的那么悬乎, 简单来说就是一个"属性管理器"

传统方式:

在早期的 js 引擎中,每个对象都背着一个 hash table. 这个table用来保存属性 key - value 的映射关系, 这种方法是松散的, 不仅占内存,而且查询 hash table 性能也低.

下图反映了 java 与传统 js 引擎属性查询方式的区别.

java 的 Class;

class Point {
 Point(int x, int y) {
 this.x = x;
 this.y = y;
 }
private:
 int x;
 int y;
}
Point p1 = new Point(10, 11);
Point p2 = new Point(12, 13);

javascript 的 Class;

function Point(x, y) {
 this.x = x;
 this.y = y;
}
var p1 = new Point(10, 11);
var p2 = new Point(12, 13);

当 java 使用 new 来创建一个 Class 实例的时候 实例 p1 会有一个 meta class 指向 Point ,java 不是动态语言,所以这个 Point 可以是内存中的一个固定地址.当 p1.x 访问属性时, 直接从 Point 上获取 x 的偏移值.效率很高.

这一切都是托了 "静态语言" 的福, 类型是固定的,一旦创建不可更改.

但 javascript 显然不是这样. 类型可以被随时更改. 比如想给 Point 增加一个 y 属性.那么直接输入 p1.y=20 就行,或者改变 x 的数据类型为 function 等等..这都可以, 这似乎与上面的方法无缘了. 所以大多数 javascript 引擎使用 hash table 来保存元数据.

下图为基于 hash table 的 js 引擎读取属性方式.

为了避免 hash table 查询, v8 采用了固化偏移量的方法,即 每一个 js 对象都会有一个隐藏的 c++ 对象与之对应(meta class 或叫 hidden class,这点与 java 类似), 当 js 要读取某个属性时, 直接在他的 hidden class 中取出对应偏移量,从而避免查表,但是这样如何应对 js 类型变化呢?

还是上面的例子:

function Point(x, y) {  << 1
 this.x = x;  << 2
 this.y = y;  << 3
}
var p1 = new Point(10, 11);   << 4
var p2 = {};
p2.x=10;
p2.y=11;

当运行到 <<1 时, V8 创建一个无属性空的隐藏类 Class0. 此时 p1 对应的隐藏类就是这个Class0.

当运行到 <<2 时 this.x = x; p1 增加 x 属性. Class0 增加一个add:x 指针,指向新的隐藏类 Class1 .在 Class1 中 x 属性的偏移量是 0. 同时修改 p1 的隐藏类为 Class1.

当运行到 <<3 时 this.y = y; p1 增加 y 属性. 我们如法炮制, Class1 增加 add:y 指针. 指向新 Class2. 修改 p1 的隐藏类为 Class2.

这样每个 js对象都有一个对应的隐藏类.

再来看看如何提取对象值.

var x = p1.x;

1.我们找到 p1 的隐藏类 Class2.

2.在 Class2 中提取 x 属性对应的内存偏移量,这里是 0.

3.取出对应偏移量的值.

V8 中所有的隐藏类都是已知类型, 且在内存中是连续的, 所以对象属性读取速度那是真心快啊..

这里我发现有一篇 貘大 的博文 V8 Hidden Class 非常好的描述了这个问题.感兴趣的同学可以 点击这个传送门

V8的 JIT编译 (JIT Compile)

V8 抛弃了字节码

所以

源码 => 抽象语法树(AST) => 字节码 => 平台相关二进制程序

变成了

源码 => 抽象语法树(AST) => 平台相关二进制程序

所以严格从结构上讲, V8并不是纯粹的虚拟机.

留言:

称呼:*

邮件:

网站:

内容: