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中的执行流程:
- js源码 => 抽象语法树AST => 字节码 然后被解释执行, 同时开始收集类型信息,为后续的 JIT阶段做准备.
- 经过运行一段时间后,发现某段代码调用次数很多(hot code). 便启动 Baseline 开始快速编译这一段代码, 时间有限,编译出的代码或许不是最优的. 这里 时间 > 性能
- 再经过一段时间后,发现某段代码调用次数仍然很多(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并不是纯粹的虚拟机.