手机版

JavaScript深入研究了V8引擎和编写优化代码的五个技巧

时间:2021-08-22 来源:互联网 编辑:宝哥软件园 浏览:

总结

JavaScript引擎是执行JavaScript代码的程序或解释器。JavaScript引擎可以实现为标准解释器或即时编译器,以某种形式将JavaScript编译成字节码。

实现JavaScript引擎的流行项目列表:

V8——开源,由谷歌开发,用C Rhino编写——由Mozilla Foundation管理,开源,完全用Java开发——是第一个支持Netscape Navigator的JavaScript引擎。目前,JavaScript内核正在被开源的Firefox使用,并以Nitro的形式出售。苹果为Safari开发了KJS-KDE的引擎。最初,Harri Porten在KDE项目中为Konqueror网络浏览器开发了Chakra(jscript 9)-Internet Explorer Chakra(JavaScript)-Microsoft Edge Nashorn,作为OpenJDK的一部分。Oracle Java语言与工具组编写的Jerry脚本——物联网轻量级引擎为什么要创建V8引擎?

谷歌打造的V8引擎是开源的,用C语言编写.这个引擎在Google Chrome中使用,但是与其他引擎不同的是,V8也在流行的node.js中使用

V8最初是为了提高web浏览器中JavaScript执行的性能而设计的。为了提高速度,V8将JavaScript代码转换成更高效的机器代码,而不是使用解释器。它通过实现JIT (Just-In-Time)编译器,在执行过程中将JavaScript代码编译成机器代码,就像很多现代的JavaScript引擎(比如SpiderMonkey或者Rhino (Mozilla))。这里的主要区别是V8不生成字节码或任何中间代码。

V8过去有两个编译器

在5.9版本出来之前,V8引擎使用了两个编译器:

full-code gen-一个简单且非常快速的编译器,可以生成简单且相对较慢的机器代码。曲轴-一个即时优化编译器,生成高度优化的代码。V8引擎内部也使用多线程:

主线程执行您所期望的:获取代码,编译代码并执行它。还有一个单独的线程用于编译,因此主线程可以继续执行Profiler线程,而前者优化代码。它会告诉运行时我们花了很多时间,这样曲轴就可以优化它们。一些线程处理垃圾收集器。第一次执行JavaScript代码时,V8使用full-codegen编译器直接将解析后的JavaScript翻译成机器码,无需任何转换。这允许它非常快速地开始执行机器代码。请注意,V8不使用中间字节码,因此不需要解释器。

代码运行一段时间后,分析线程已经收集了足够的数据来判断应该优化哪种方法。

接下来,曲轴从另一个螺纹开始优化。它将JavaScript抽象语法树转换成一个高级的静态单一赋值(SSA)表示,称为氢,并试图优化氢图,其中大部分都是在这个级别完成的。

内嵌代码

第一个优化是提前内联尽可能多的代码。内联是用被调用函数的主体替换调用点(调用函数的代码行)的过程。这个简单的步骤使得下面的优化更有意义。

隐藏类

JavaScript是一种基于原型的语言:类和对象不是使用克隆过程创建的。JavaScript也是一种动态编程语言,这意味着属性可以在实例化后很容易地从对象中添加或删除。

大多数JavaScript解释器使用类似字典的结构(基于哈希函数)来存储内存中对象属性值的位置。这种结构使得用JavaScript检索属性值比用Java或C #等非动态编程语言检索属性值更昂贵。

在Java中,所有对象属性都是由编译前的固定对象布局决定的,不能在运行时动态添加或删除(当然C #有动态类型,这是另一个话题)。

因此,属性值(或指向这些属性的指针)可以作为连续的缓冲区存储在内存中,每个缓冲区之间有固定的偏移量,偏移量的长度可以根据属性类型轻松确定,这在运行时可以更改属性类型的JavaScript中是不可能的。

因为使用字典查找对象属性在内存中的位置效率非常低,所以V8使用了一种不同的方法:隐藏类。隐藏类的工作方式类似于Java等语言中使用的固定对象(类),只是它们是在运行时创建的。现在,让我们看看他们的实际例子:

一旦“新点(1,2)”调用发生,V8将创建一个名为“C0”的隐藏类。

尚未为点定义属性,因此“C0”为空。

一旦第一个语句“this.x=x”被执行(在“Point”函数中),V8将创建第二个名为“C1”的隐藏类,它基于“C0”。“C1”描述了内存中(相对于对象指针)可以找到属性x的位置。

在这种情况下,“x”存储在偏移量0处,这意味着当内存中的点对象被视为连续缓冲区时,第一个偏移量将对应于属性“x”。V8还会用“类变换”更新“C0”,表示如果点对象增加属性“x”,隐藏类应该从“C0”切换到“C1”。以下点对象的隐藏类现在是“C1”。

每次向对象添加新属性时,旧的隐藏类都会更新为指向新隐藏类的转换路径。隐藏类转换很重要,因为它们允许隐藏类在以相同方式创建的对象之间共享。如果两个对象共享一个隐藏类,并且向它们添加了相同的属性,则转换将确保这两个对象接收到相同的新隐藏类以及伴随它的所有优化代码。

当执行语句“this.y=y”时,重复相同的过程(在“Point”函数内部,在语句“this.x=x”之后)。

将创建一个名为“C2”的新隐藏类。如果一个属性“Y”被添加到一个点对象(它已经包含属性“X”)并且一个类转换被添加到“C1”,隐藏类应该被改变为“C2”并且点对象的隐藏类被更新为“C2”。

隐藏的类转换取决于向对象添加属性的顺序。请看下面的代码片段:

现在,假设p1和p2将使用相同的隐藏类和转换。然后,对于“p1”,先添加属性“a”,再添加属性“b”。然而,“p2”首先被分配“b”,然后被分配“a”。因此,由于转换路径不同,“p1”和“p2”在不同的隐藏类别中结束。在这种情况下,最好以相同的顺序初始化动态属性,以便可以重用隐藏的类。

内嵌缓存

V8利用了另一种优化动态类型语言的技术,称为内联缓存。内联缓存依赖于这样的观察:对同一方法的重复调用经常发生在同一类型的对象上。这里可以找到对内联缓存的深入解释。

接下来,我们将讨论内联缓存的一般概念(如果您没有时间了解更多)。

那么它是如何工作的呢?V8维护在最近的方法调用中作为参数传递的对象类型的缓存,并使用该信息来预测将来作为参数传递的对象类型。如果V8能够很好地预测传递给方法的对象的类型,那么它就可以绕过如何访问对象属性的过程,使用之前查找到的存储信息来查找对象的隐藏类。

那么隐藏类和内联缓存的概念是如何关联的呢?每当对特定对象调用方法时,V8引擎必须搜索该对象的隐藏类,以确定访问特定属性的偏移量。在两次成功调用同一个隐藏类后,V8省略了对隐藏类的搜索,只是将该属性的偏移量添加到对象指针本身。对于该方法的所有后续调用,V8引擎假设隐藏类没有改变,并使用从先前查找中存储的偏移量直接跳转到特定属性的内存地址。这大大提高了执行速度。

内联缓存也是相同类型的对象共享隐藏类非常重要的原因。如果您创建了两个相同类型的对象和不同的隐藏类(就像我们在前面的例子中所做的那样),V8将无法使用内联缓存,因为即使这两个对象属于相同的类型,它们对应的隐藏类也会为它们的属性分配不同的偏移量。

这两个对象基本相同,但是创建“a”和“b”属性的顺序不同。

编译成机器代码

氢气图优化后,曲轴将其简化为一个更低层次的表示,称为锂。大多数锂电实现都是特定于架构的。寄存器分配经常发生在这个级别。

最后,锂被编译成机器代码。然后是OSR:堆叠替代。在我们开始编译和优化显式长时间运行的方法之前,我们可能会运行堆栈替换。V8不只是慢慢执行栈替换,重新开始优化。相反,它会转换我们拥有的所有上下文(栈、寄存器),以便在执行过程中切换到优化版本。这是一项非常复杂的任务,因为除了其他优化之外,V8最初还内联了代码。V8不是唯一能做到这一点的发动机。

有一种称为去优化的安全措施,它会进行相反的转换,并在假设引擎无效的情况下返回未优化的代码。

碎片帐集

对于垃圾收集,V8使用传统的标记和清除算法来清理旧的一代。标记阶段应该停止JavaScript的执行。为了控制GC开销,使执行更稳定,V8采用增量标记:不遍历整个堆,而是尝试标记每一个可能的对象,只遍历堆的一部分,然后恢复正常执行。下一次垃圾收集停止将从上次堆遍历停止的地方继续,这允许在正常执行过程中有一个非常短的暂停。如前所述,扫描阶段由单独的线程处理。

如何编写优化的JavaScript

对象属性的顺序:对象属性总是以相同的顺序进行实例化,以便可以共享隐藏的类和随后优化的代码。动态属性:因为在实例化后给对象添加属性会强制隐藏类的更改,并减慢之前被隐藏类优化的所有方法的执行速度,所以在它们的构造函数中分配所有对象的属性。方法:重复执行相同方法的代码将比只执行一次多个不同方法的代码运行得更快(由于内联缓存)。数组:避免稀疏数组,在稀疏数组中键值不是自增数字,不存储所有元素的稀疏数组是哈希表。该数组中的元素访问开销很高。此外,尽量避免预分配大型阵列。最好按需增长。最后,不要删除数组中的元素,这会使键值变得稀疏。标记值:V8用32位表示对象和值。由于该值为31位,因此它使用一位来区分它是对象(标志=1)还是名为SMI(SMall Integer)的整数(标志=0)。然后,如果一个数字超过31位,V8将把这个数字装箱,变成一个双精度数字,并创建一个新的对象来存储这个数字。尽可能使用31位有符号数,避免JS对象的高开销装箱操作。点火和涡扇发动机

随着2017年初V8 5.9的发布,引入了新的执行管道。这种新的管道在实际的JavaScript应用程序中实现了更大的性能提升和显著的内存节省。

新的执行流程基于Ignition(V8解释器)和涡扇(V8最新优化编译器)。

自5.9版本发布以来,由于V8团队一直在努力跟上新的JavaScript语言特性以及这些特性所需的优化,V8团队已经停止使用full-codegen和曲轴(自2010年以来一直服务于V8技术)。

这意味着V8作为一个整体将有一个更简单和更易维护的架构。

这些改进只是开始。新的Ignition和涡扇管道为进一步优化铺平了道路,这将提高JavaScript性能,并在未来几年内减少V8在Chrome和Node.js中的占用空间。

以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。

版权声明:JavaScript深入研究了V8引擎和编写优化代码的五个技巧是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。