原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized codejavascript
几周前咱们开始了一个系列博文旨在深刻挖掘 JavaScript
并弄清楚它的工做原理:咱们认为经过了解 JavaScript
的构建单元并熟悉它们是怎样结合起来的,有助于写出更好的代码和应用。html
这个系列的第一篇文章聚焦于提供一个关于引擎、运行时和调用栈的概述。本文将会深刻分析 Google
的 V8
引擎的内部实现。咱们也会提供一些编写更优质 JavaScript
代码的小技巧——咱们的团队在构建 SessionStack
应用时遵循的最佳实践。java
JavaScript
引擎是执行 JavaScript
代码的程序或解释器。 JavaScript
引擎能够实现为标准的解释器,或即时编译器,以某种形式将 JavaScript
编译成字节码。git
如下是一些流行的 JavaScript
引擎项目:github
Google
开发,C++
编写Mozilla
基金会管理,开源,彻底使用 Java
开发JavaScript
引擎,之前由 Netscape Navigator
维护,如今由 Firefox
维护Nitro
的名义销售,由 Apple
公司为 Safari
浏览器开发 KDE
的引擎,最初由 Harri Porten
为 KDE
项目的 Konqueror
浏览器开发IE
浏览器Edge
浏览器OpenJDK
开源项目的一部分,由 Oracle Java
和其工具集开发谷歌公司研发的 V8
引擎是由 C++
编写的开源引擎。该引擎使用在谷歌浏览器内部。但与其余引擎不一样的是,V8
也应用于 Node.js
这一流行的运行时当中。编程
V8
最初是为了提升浏览器中 JavaScript
执行的性能而设计的。为了得到速度,V8
将 JavaScript
代码转换成更高效的机器编码而不是使用解释器。同其余现代 JavaScript
引擎如 SpiderMonkey
或 Rhino
(Mozilla
)所作的同样,V8
经过实现即时编译器在执行时将 JavaScript
代码编译成机器代码。其中最主要的区别是 V8
不生成字节码或任何中间代码。数组
在 V8
5.9版本发布以前(2017年初发布),该引擎使用两个编译器:浏览器
同时 V8
内部使用了多条线程:缓存
Crankshaft
编译器优化代码首次执行 JavaScript
代码时,V8
利用 full-codegen
无过渡地直接将解析后的 JavaScript
转换成机器代码。这使得它能够很是快速地开始执行机器代码。注意 V8
不使用中间代码表示,所以摆脱了对解释器的须要。安全
在你的代码运行了必定时间后,分析线程就能收集到足够的数据判断哪些方法须要优化。
接着,Crankshaft
优化在另外一线程开始。它将 JavaScript
抽象语法树转换成高级静态单赋值(SSA
)表示,称为 Hydrogen
(注:氮),并尝试优化氮图。大多数优化都在这个级别完成。
优化的第一步是先内联尽量多的代码。内联是一个将调用引用(函数调用的那行代码)替换成所调用的函数体的过程。这个简单的步骤使接下来的优化过程更有意义:
JavaScript
是基于原型的语言:没有类,使用克隆的方式建立对象。JavaScript
仍是一个动态编程语言,这意味着当对象被初始化以后还能够轻易地增删其属性。
大多数 JavaScript
解释器采用类字典数据结构(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得在 JavaScript
中取回属性值的计算开销比非动态语言如 Java
或 C#
更昂贵。在 Java
中,全部的对象属性在编译前就由固定对象布局决定了,不容许在运行时动态增长或删除(C#
有动态类型,但那是另外一个话题)。所以,属性值(或指向属性的指针)就能够以连续缓冲区存储在内存中,之间用固定的偏移量隔开。偏移量的长度简单地根据属性的类型肯定,然而这在 JavaScript
中是不可能的,由于属性类型能够在运行时更改。
因为经过字典查找对象属性在内存中的位置很是低效,V8
采用了另外一方法做为替代:隐藏类。隐藏类的原理相似于 Java
等语言中使用的固定对象布局(类),除了是在运行时建立。如今,让咱们来看看它们实际是什么样的:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
当 new Point(1, 2)
调用发生,V8
将建立了一个名为 C0
的隐藏类。
如今 Point
尚未定义任何属性,因此 C0
是空的。
一旦第一条声明 this.x = x
开始执行(在 Point
函数内),V8
将建立第二个基于 C0
的隐藏类 C1
。C1
描述了在内存中(相对于 point
对象)能找到属性 x
的位置。在这个例子中,x
保存在偏移量为 0
的位置,这意味着在将内存中的对象视做一个连续缓冲区时,第一个偏移量对应着 x
。V8
还会经过一个“类转换”更新 C0
,以代表若是一个属性 x
被添加到 point
对象中,隐藏类 C0
就会转换成 C1
。下面 point
对象的隐藏类如今变成了 C1
。
每次添加一个新属性到对象,旧隐藏类都会经过一个转换路径更新成一个新隐藏类。隐藏类转换之因此如此重要是由于它能使隐藏类在以一样方式建立的对象间共享。若是两个对象共享同一个隐藏类并向它们添加相同的属性,转换能够确保它们得到相同的隐藏类和全部与其相关的优化代码。
当 this.y = y
语句执行时将会重复一样的过程(一样在 Point
函数内,this.x = x
以后)。
新的隐藏类 C2
将被建立,C1
发生类转换表示若是向一个 Point
对象添加属性 y
(已经包含一个属性 x
),隐藏类应该更新为 C2
,而且 point
对象的隐藏类更新为 C2
。
隐藏类转换依赖向对象所添加属性的顺序。请看下面的代码片断:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2); p1.a = 5; p1.b = 6; var p2 = new Point(3, 4); p2.b = 7; p2.a = 8;
如今你可能会假设 p1
和 p2
使用相同的隐藏类和转换。实际则并不是如此。对于 p1
,先添加属性 a
而后添加属性 b
。而对于 p2
,先添加的属性是 b
而后才是 a
。所以,因为转换路径不一样, p1
和 p2
最终将会产生不一样的隐藏类。在这种状况下,最好在初始化动态属性时保持顺序一致以便复用相同的隐藏类。
V8
利用了另外一项叫作内联缓存的技术来优化动态类型语言。内联缓存依赖于这样一种观察:同一方法的重复调用一般发生在同一类型的对象上。关于内联缓存的深刻阐述在这里。
咱们准备介绍内联缓存的通常概念(以避免你没有时间查看上述的深刻阐述)。
那么它的原理是什么?V8
维护着在最近的方法调用中做为参数传入的对象类型的缓存,并利用这个信息假设将来会被当作参数的对象的类型。若是 V8
能很好地假设出将要传入方法的对象的类型,就能直接越过如何获取对象属性的计算过程,取而代之的是使用以前查找对象的隐藏类时存储的信息。
那么隐藏类是如何与内联缓存关联起来的?每当某一对象调用方法时,V8
必须执行对此对象的隐藏类的查询来肯定访问某个属性的偏移量。当对同一隐藏类成功调用过两次一样的方法后,V8
将省略对隐藏类的查询而只将属性偏移量添加到对象指针自己。对于那个方法将来全部的调用,V8
都假定隐藏类不改变,并利用以前查询存储的偏移量直接跳到某一属性的内存地址。这极大地提升了执行速度。
内联缓存也是同类对象共享同一隐藏类如此重要的缘由。若是你建立了拥有不一样隐藏类的两个同类对象(正如前面的例子),V8
就没法使用内联缓存,由于即使这两个对象是相同的类型,但他们对应的隐藏类为属性指定了不一样的偏移量。
这两个对象基本相同,但a
、b
属性的建立顺序不一样。
一旦氮图优化好后,Crankshaft
会将它降为更低水平的表示,称为 Lithium
(注:锂)。大多数 Lithium
的实现依赖于特定架构。寄存器分配发生在这个级别。
最终,Lithium
被编译成机器代码。随后发生 OSR
:堆栈上替换。在开始编译和优化明显长时间运行的方法前,咱们可能会运行它。V8
不会在再次开始执行优化版本时忘记那些缓慢的执行。而是转换咱们全部的上下文(栈,寄存器)以便能在执行中切换到优化版本。这是个很是复杂的任务,记住在其余的优化中,V8
最早作了代码内联。V8
不是惟一有这种能力的引擎。
还有种被称为反优化的安全措施能作反向转换,回退到未优化代码,以防引擎作出的假设再也不成立。
在垃圾回收方面,V8
采用传统分代方法标记和清扫来清理老的代。标记阶段会暂停 JavaScript
的执行。为了控制垃圾回收的开销并使执行更加稳定,V8
采用增量标记:它不遍历所有栈堆,而是尝试标记每个可能的对象,它只遍历栈堆的一部分,而后恢复正常执行。下一次垃圾回收暂停会在以前栈堆的中止位置继续。这可以使正常执行期间只发生至关短的暂停。正如以前提到的,清理阶段由单独的线程处理。
随着2017年初 V8
5.9版本的发布,一个新的执行管道被引入。新的管道在实际的JavaScript
应用中实现了更大的性能提高和的显著的内存节省。
新的执行管道构建在 V8
的解释器 Ignition
和 V8
最新的优化编译器 TurboFan
之上。
你能够在这里查阅 V8
团队关于这个主题的博文。
自从 V8
5.9版本发布以来, V8
就再也不在 JavaScript
执行里使用 full-codegen
和 Crankshaft
(自2010年来一直支撑着 V8
的技术),这是因为 V8
团队也在努力地跟上新的 JavaScript
语言特性的脚步和这些特性所需的优化。
这意味着未来在总体上 V8
将拥有更加简单和更易于维护的架构。
这些提高仅仅是个开始。新的 Ignition
和 TurboFan
管道铺垫了更远的优化之路,将会推动 JavaScript
的性能并在接下来的几年里缩小 V8
在 Chrome
和 Node.js
中的足迹。
最后,这里有几条关于如何编写更优化的、更好的 JavaScript
代码的建议和技巧。虽然你能够很容易地从上述的内容中获得这些,为了方便仍是把它们作了如下的总结:
V8
用32位字节表示对象和数字。其中使用了一个位来标识是对象(标识为1)或是整数(标识为0),因为它们是31位的而被称为 SMI
(SMall Integer
)。若是一个数值大小超过了31位能够表示的数字,V8
将会包装它,将其转换为一个双字节类型值并建立一个新的对象存入其中。尽可能使用31带符号的数值避免 JS
对象的昂贵包装操做。