点击上方蓝字关注我们
本系列的第一篇文章重点介绍了引擎、运行时和调用堆栈。第二篇文章将深入探讨Google V8 JavaScript引擎的内部部分。
概述
JavaScript引擎是执行JavaScript代码的程序或解释器。JavaScript引擎可以实现为标准解释器,也可以实现为以某种形式将JavaScript编译为字节码的实时编译器。
这是一个正在实现JavaScript引擎的流行项目列表:
V8-- 开源引擎,由谷歌开发,用C++编写
Rhino-由Mozilla基金会管理,开源,完全用Java开发
SpiderMonkey-第一个JavaScript引擎,以前支持Netscape Navigator,现在支持Firefox
JavaScriptCore-开源,由Apple开发用于Safari
KJS-KDE的引擎最初由Harri Porten为KDE项目的Konqueror web浏览器开发
Chakra(JScript 9)-Internet Explorer
Chakra(JavaScript)-Microsoft Edge浏览器引擎
Nashorn,作为OpenJDK的部分的开源项目,由Oracle Java语言和工具组编写
JerryScript 是一个轻量级的物联网引擎
V8引擎的故事
谷歌开发的V8引擎是开源的,用C++编写。这个引擎通常在Google Chrome中使用。然而,与其他引擎不同,V8也适用于流行的Node.js。
V8最初是为了提高web浏览器中JavaScript执行的性能而设计的。为了提高js执行速度,V8将JavaScript代码直接转换为更高效的机器代码,而不是使用解释器。它在执行时通过实现JIT(即时)编译器将JavaScript代码编译成机器代码,就像许多现代JavaScript引擎(如SpiderMonkey或Rhino(Mozilla))那样。这里的主要区别是V8不产生字节码或任何中间代码。
V8 使用了两个编译器
在V8引擎的v5.9版本问世之前,V8引擎使用了两个编译器:
full compile: full compiler是不含优化的编译器,目标是快速地生成原生代码,以保持页面始终快速运转,所以full compiler省去了将语法树(AST)转换为字节码的过程,直接生成原生代码。
Crankshaft:由于full compiler没有对代码进行优化,所以V8引入了crankshaft编译器,通过数据分析器来挑选使用频率高的函数来进行优化,生成高效的原生代码,但是鉴于JavaScript是一门动态类型语言,很有可能在程序运行过程中进行类型改变,所以V8会将crankshaft编译过的代码进行优化回滚,直至回滚到当前最优状态。
V8引擎还同时使用了几个线程:
主线程执行你预期的操作:获取你的代码,编译它,然后执行它
还有一个单独的线程用于编译,这样主线程可以在前者编译代码时继续执行
一个分析器线程,它将告诉运行时我们花费了大量时间在哪些方法上,以便Crankshaft可以优化它们
处理Garbage Collector(即垃圾回收)的几个线程
当第一次执行JavaScript代码时,V8通过full-codegen直接将解析的JavaScript转换为机器代码,而无需任何转换。这使得它可以非常快地开始执行机器代码。注意,V8不使用中间字节码表示,这样就不需要解释器。
当您的代码运行一段时间后,探查器线程已经收集了足够的数据来判断应该优化哪个方法。
接下来,Crankshaft从另一个线程开始。它将JavaScript抽象语法树转换为一个名为Hydrogen的高级静态单赋值(SSA)表示,并尝试优化Hydrogen图。大多数优化都是在这个级别完成的。
内联
第一个优化是预先内联尽可能多的代码。内联是用被调用函数的主体替换调用位置(调用函数的代码行)的过程。这个简单的步骤将会使下面的优化更有意义。
隐藏类
JavaScript是一种基于原型的语言:没有使用克隆的方式创建类和对象。JavaScript也是一种动态编程语言,这意味着在对象实例化后,可以很容易地从对象中添加或删除属性。
大多数JavaScript解释器使用类似字典的结构(基于散列函数)来存储对象属性值在内存中的位置。这种结构使得在JavaScript中检索属性的值比在Java或C#等非动态编程语言中检索属性的值在计算上更为昂贵。在Java中,所有的对象属性在编译之前都由一个固定的对象布局决定,并且不能在运行时动态添加或删除(好吧,C#具有另一个主题的动态类型)。因此,属性(或指向这些属性的指针)的值可以作为一个连续缓冲区存储在内存中,每个缓冲区之间有一个固定的偏移量。偏移量的长度可以很容易地根据属性类型来确定,而这在JavaScript中是不可能的,在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”描述了在内存中(相对于对象指针)可以找到属性x的位置。在这种情况下,“ x”存储在偏移量0处,这意味着在将内存中的点对象作为连续缓冲区查看时,第一个偏移量将对应于属性“ x”。V8还将使用“类转换”更新“ C0”,该类转换指出如果将属性“ x”添加到点对象,则隐藏的类应从“ C0”切换为“ C1”。现在,下面的点对象的隐藏类为“ C1”。
每次将新属性添加到对象时,都会使用到新隐藏类的过渡路径来更新旧的隐藏类。隐藏类的转换非常重要,因为它们允许隐藏类在以相同方式创建的对象之间共享。如果两个对象共享一个隐藏类,并且向两个对象都添加了相同的属性,则过渡将确保两个对象都接收到相同的新隐藏类以及随之而来的所有优化代码。
当执行语句“ this.y = y”(同样在Point函数内部,在“ this.x = x”语句之后)时,将重复此过程。
创建了一个名为“ C2”的新隐藏类,将一个类转换添加到“ C1”,表明如果将属性“ y”添加到Point对象(已经包含属性“ x”),则该隐藏类应更改为“ C2”,并且点对象的隐藏类更新为“ 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”属性是按不同顺序创建的。
垃圾回收
对于垃圾回收,V8使用传统的标记清除技术来清理旧的一代。标记阶段应该停止JavaScript执行。为了控制GC成本并使执行更加稳定,V8使用了增量标记:不是遍历整个堆而是尝试标记每个可能的对象,而是仅遍历堆的一部分,然后恢复正常执行。下一个GC停止将从上一个堆漫游已停止的位置继续。这允许程序在正常执行期间进行非常短的暂停。如前所述,回收阶段将会由单独的线程处理。
Ignition and TurboFan
2017年初V8 在5.9的发布中引入了新的执行管道。这个新的管道在实际的JavaScript应用程序中实现了更大的性能改进并显着节省了内存。
新的执行管道基于V8的解释器Ignition和V8的最新优化编译器TurboFan。
您可以在此处查看V8团队有关该主题的博客文章。
自V8的5.9版本问世以来,由于V8团队一直在努力跟上新的JavaScript语言功能,并且V8团队一直在努力与之同步,因此完整代码源和Crankshaft(自2010年起为V8服务的技术)不再被V8用于JavaScript执行这些功能所需的优化。
这意味着整个V8将会拥有更简单,更易于维护的体系结构。
这些改进仅仅是开始。新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提高JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。
最后,这是有关如何编写经过优化的更好JavaScript的一些技巧。您可以轻松地从以上内容中得出这些内容,但是,为方便起见,以下是摘要:
如何编写更优化的JavaScript
对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏的类以及随后优化的代码。
动态属性:实例化后向对象添加属性将强制更改隐藏的类,并减慢为先前的隐藏类优化的所有方法。而是在其构造函数中分配对象的所有属性。
方法:重复执行相同方法的代码将比仅执行一次许多不同方法的代码运行速度更快(由于内联缓存)。
数组:避免键不是增量数字的稀疏数组。该数组下每个元素的稀疏数组是一个哈希表,这样的数组中的元素访问起来更昂贵。另外,请尝试避免预先分配大数组。最后,不请随意要删除数组中的元素,这将使它键稀疏。
标记的值:V8代表32位的对象和数字。它使用一个位来知道它是一个对象(标志= 1)还是一个称为SMI(小整数)的整数(标志= 0),因为它有31位。然后,如果数值大于31位,则V8会将数字装箱,将其变成双精度并创建一个新对象以将数字放入其中。尽可能使用31位带符号的数字,以避免对JS对象进行昂贵的装箱操作。
写在最后
方凳雅集是由阿里巴巴B系6大BU(1688,ICBU,零售通,AE,企业金融,考拉)共同维护的公众号奥,我们会定期发送优质好文,欢迎扫码关注
求关注
求转发