JS 引擎 (0):JavaScript 引擎群雄演义—起底 JavaScript 引擎
JavaScript 既是一个 面向过程的语言 又是一个 面向对象的语言。在 JavaScript 中,通过在运行时给空对象附加方法和属性来创建对象,与编译语言如 C++ 和 Java 中常见的通过语法来定义类相反。对象构造后,它可以用作是创建相似对象的原型。
JavaScript 的动态特性包括运行时构造对象、可变参数列表、函数变量、动态脚本执行(通过 eval)、对象内枚举(通过 for ... in)和源码恢复(JavaScript 程序可以将函数反编译回源代码)。
JavaScript 方面,之前写过《ECMAScript进化史(1):话说Web脚本语言王者JavaScript的加冕历史》
在看 各JavaScript引擎的简介,及相关资料/博客收集帖 ,结合自己的理解,整理一个笔记。现代JavaScript引擎都有哪些特征呢?跟以前的 JavaScript 引擎有怎样的差别,为什么变快了那么多?
JavaScript 引擎历史
早期 JavaScript 引擎的实现普遍跟同时代的其它脚本语言一样,比较“偷懒”。反正是“脚本语言”,当时的 JavaScript 脚本通常只包含很简单的逻辑,只运行很短时间就完事。没啥性能压力,得不到足够的重视与开发资源,性能自然是好不到哪里去,却也足以满足当时的需求。
Mocha
非常早期的“Mocha”引擎实现得确实非常偷懒。字节码解释器、引用计数方式的自动内存管理、fat discriminated union 形式的值表现形式。犀牛书第4版写了点JavaScript与引用计数的历史。
SpiderMonkey
1996 年,祖师爷 Brendan Eich 新写的 SpiderMonkey 已经改为使用 mark-and-sweep GC、tagged value。
在 V8 出现前,SpiderMonkey 是 native application 嵌入 JavaScript 的最流行选择。如果大家没留意过的话,UltraEdit 就内嵌了 SpiderMonkey 来让用户使用 JavaScript 写宏与插件[/url];Adobe Acrobat 也类似。
于是其实早期的两个主要的 JavaScript 引擎实现,Mozilla SpiderMonkey 和 Microsoft JScript 其实都一直在用 mark-and-sweep GC。也没啥别的主流 JavaScript 引擎用过引用计数方式来实现自动内存管理的。这点别被忽悠了。
在叫得出名字的 JavaScript 引擎里只有quad-wheel(没听说过么?不奇怪,非主流嘛)是用引用计数方式实现自动内存管理的。
老版本 IE 里 JScript 虽说是有因为循环引用而导致内存泄漏的问题,但那不是因为 JScript 自身用引用计数。问题出在 JScript 与 DOM 交互的边界上:IE 的 DOM 节点(及其它 host 对象)是 COM 对象,而 COM 对象自身是引用计数的。在 JS 一侧 GC 时 DOM 节点被看作根节点,所以被 DOM 节点引用的 JS 对象不会死;反过来,被 JS 对象引用的 DOM 节点的引用计数不为 0 所以也不会死。这导致 JScript 与 DOM 交互时有可能被连累引发循环引用->内存泄漏的问题。
IE9/Chakra 里已经通过把 DOM 对象变成由 JavaScript 一侧的 GC 来管理解决了这个问题。
早期 JavaScript 引擎得到的投入实在不足,而当时的 Java 虚拟机(JVM)却得到了大量资源实现各种优化,包括 JIT 编译器之类。这使得用 Java 写的 Rhino 一度能比用 C 写的 SpiderMonkey 跑得还快,因为 Rhino 得益于 JVM 里优秀的 JIT 编译器和 GC,而 SpiderMonkey 还在用简易的解释器和 GC。
这个阶段中,JavaScript 对象的布局或者说表现方式通常可以叫做“property bag”,本质上就跟 hashmap 一样。
Rhino/Nashorn
Rhino 是 Java 版的 SpiderMonkey。当时 Netscape 想用纯 Java 来实现新版浏览器,自然需要一个 Java 版的 JavaScript 引擎实现;另外也希望能在服务器端把 JavaScript 当作 Java 应用里的脚本语言使用。于是 Rhino 就诞生了。
具体查看《Java集成JavaScript项目工程:基于Rhino的javascript后台开发》
KJS
Apple 把 KHTML 拿去演化出了 WebKit,其中的 KJS 演化成了 JavaScriptCore。KJS 影响力远不如 JavaScriptCore。KJS 是为数不多的没有 JIT 编译器的。
文档: http://api.kde.org/4.x-api/kdelibs-apidocs/tier1/kjs/src/kjs/html/index.html
兼容标准: ECMAScript 3
代码: https://projects.kde.org/projects/kde/kdelibs/repository/revisions/master/show/kjs
JavaScriptCore
JavaScriptCore 源自 KJS,但持续得到苹果的大力投入,终而青出于蓝胜于蓝,已经完全超越了它的前身。
QtScript 背后也使用 JavaScriptCore。
虽然 iOS 的 Safari 和 UIWebView 控件里跑的都是JavaScriptCore,但只有 Apple 自己的程序才可以启用 JIT 编译,而第三方的则不行。所以 Mobile Chrome for iOS 就用不了 JavaScriptCore 的 JIT。
Chakra
Chakra 问世后的 JScript 已非当日吴下阿蒙。
即便 Chakra 的解释器也是字节码解释器,它的字节码设计与老版本 JScript 的已经相当不同,解释器自身的速度都已经有所提升。
Chakra 里的隐藏类变迁机制叫做“type evolution”。每个产品都必须发明些新名词
E9 版 Chakra 里字段数量不超过 16 个的对象可以使用紧凑布局;IE10 版 Chakra 将这限制放宽到 30 多个字段。
IE9 Chakra 的对象布局是对象头与 property 数组分离的。IE10 版则将构造器函数里赋值的属性直接跟对象头粘在一起分配。
Chakra 里的 value representation 跟 V8 的比较类似,都是在最低的几位放 tag;不过 Chakra 的是 tagged-value,也就是在小整数的后面带上一个 0x1 的 tag,而对象地址是 8 字节对齐的于是对象指针的最低 3 位为 0。打 tag 的取舍正好与 V8 的 tagged-pointer 相反,而与更多其它用 tagged-value 的 VM 相似,例如说更传统的 Smalltalk 实现,包括现在还可以用到的 Squeak,或者是像 Ruby 等受 Smalltalk 影响的 VM。
注意:IE9 在 x64 上的版本里的 Chakra 只有解释器,没实现 JIT 编译器;到 IE10 才开始在 x64 版上提供 JIT 编译器。
同样只有字节码解释器,IE9 64-bit 的 Chakra 仍然可以比 IE8 64-bit 的 JScript 5.8 快近 10 倍
JScript
JScript 5.8(IE8 里的 JScript)之后版本号重新计算了,下一个大版本就是 IE9 里的 JScript 9.0,代号 Chakra,在前面有介绍。
JScript 里对象里属性的存储基本上是靠 Hashtable;数组性质的对象最初也是为稀疏数组优化,背后仍然是用 Hashtable 来存储。到 IE8/JScript 5.8 才加上了对密集数组的存储/访问优化。
兼容标准: ECMAScript 3.0
执行引擎是个简单的解释器,switch-threading 形式的解释器主循环,位于 CScriptRuntime::Run(VAR*)。在 jscript.dll 里这个 switch 被编译为一个 table-based dispatch。
被这两处调用:
ScrFncObj::CallWithFrameOnStack(VAR *,int,VAR *,VAR *,ulong)
ScrFncObj::Call(VAR *,int,VAR *,VAR *,ulong)
用于优化字符串拼接用的 BuildString 类。在 Chakra 里也继承了下来。
不常见的 JavaScript 引擎
上面的 JavaScript 引擎都是常见
IronJS
IronJS 原本完全使用 F#实现,后来改为只用 F#来实现 parser,而用 C#来实现 runtime 部分。这是个非常妙的搭配。F#(以及许多函数式语言)天生就非常适合用来写需要大量模式匹配的程序,写 parser 最适合不过。而 runtime 部分更多是与.NET 的其它部分打交道,这里用 C#就会更顺手些。
Ironjs 是在 Microsoft 动态语言运行时之上构建的 ECMAScript 3.0 实现,它使您可以将 JavaScript 运行时嵌入到.NET 应用程序中。
使用 Ironjs 环境
.NET 3.5(Src / CLR2.sln)
.NET 4.0(Src / CLR4.sln)
Mono 2.10(Src / mono-build.sh)
Ironjs 还具有对.NET 2.0 和 3.0 的实验性支持,可使用 CLR2 解决方案进行编译并设置额外的 NET2 标志。
IronJS 的 parser 整体采用 top-down operator precedence(TDOP)方式,在 JavaScript 的引擎实现中比较少见。不过却正好与微软自家的 Managed JScript 相似。不知道作者在写 IronJS 时是否有受 Managed JScript 的思路影响呢?
如果采用 TDOP 不是 Managed JScript 的影响,那或许是受Douglas Crockford大神那篇TDOP教程的影响了。
最初的 IronJS 其实用的是基于 ANTLR 生成的 parser。不过后来用 F#新写的 parser 比老的 ANTLR 生成的 parser 快得多。
不过作者决定在下一版 IronJS 里改为完全使用 C#,主要是出于性能方面的考虑。并不是 F#本身不够快,而是 F#的各种方便简洁的功能容易引人写出不那么快的代码,而要写比较高效的代码样子会跟 C#看起来很像。于是还不如直接用 C#好了。
IronJS 使用了 Nan-boxing,只不过比起那些用 C/C++之类的 native 语言所实现的 NaN-boxing tagged pointer 而言,IronJS 版的比较“肥”一些——例如说 JavaScriptCore 的一个 tagged pointer 在 x86-64 上就是 64 位,跟一个 double 一样大,指针类型的值跟值类型的值可以重叠在同一个位置上;而在 IronJS 的则要 128 位,其中值类型的值与 tag 在头 64 位,而指针类型在后 64 位。
虽然肥一些,作为 Nan-boxing 的思路和效果还是类似的。用了 tagged pointer 之后至少那些值类型的值的内存开销都变小了——不用 tagged pointer 的话自动装箱的 double 在 32 位 CLR 上也至少得要 16 字节,外加引用它的指针 4 字节也得要 20 字节了,而 IronJS 的 BoxedValue 则总共只要 16 字节而且不会有额外指针带来的间接层,在内存局部性上也比不用 tagged pointer 好。
参考文章:
关于 JavaScript https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/About_JavaScript
各 JavaScript 引擎的简介,及相关资料/博客收集帖 https://hllvm-group.iteye.com/group/topic/37596
用 JavaScript 解释 JavaScript 虚拟机-内联缓存(inline caches) https://segmentfault.com/a/1190000010819044
GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,优化收集方法的思路 https://blog.csdn.net/fateruler/article/details/81158510
转载本站文章《JS引擎(0):JavaScript引擎群雄演义—起底JavaScript引擎》,请注明出处:https://www.zhoulujun.cn/html/webfront/browser/webkit/2020_0718_8521.html
评论