JavaScript如何工作:在V8引擎内部+5个提示如何编写优化的代码

2017-10-19 09:17


该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。这第二篇文章将潜入Google V8 JavaScript引擎的内部部分。我们还将提供一些关于如何在构建产品时如何在SessionStack中编写更好的JavaScript代码实践我们的开发团队的快速提示

概观

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

这是一个正在实施JavaScript引擎的热门项目列表:

  • V8  - 开源的,由Google开发的,用C ++编写
  • Rhin o  由Mozilla基金会管理,开源,完全用Java开发
  • SpiderMonkey  - 第一个JavaScript引擎,它回溯在日子供电的Netscape Navigator中,今天强加了Firefox
  • JavaScriptCore  - 开源,由Nitro推出,由苹果公司开发用于Safari
  • KJS  - KDE的引擎最初由Harri Porten开发,用于KDE项目的Konqueror网络浏览器
  • Chakra(JScript9)  - Internet Explorer
  • Chakra(JavaScript)  - Microsoft Edge
  • Nashorn,OpenJDK的一部分开源,由Oracle Java语言和工具组编写
  • JerryScript  - 是物联网的轻量级引擎。

为什么V8引擎创建?

由Google构建的V8引擎是开源的,用C ++编写该引擎在Google Chrome中使用。然而,与其他引擎不同,V8也用于流行的Node.js运行时。

 

V8首先旨在提高Web浏览器中JavaScript执行的性能。为了获得速度,V8将JavaScript代码转换为更有效的机器代码,而不是使用解释器。它通过实施像许多现代JavaScript引擎(如SpiderMonkey或Rhino(Mozilla))JIT(即时)编译器,将JavaScript代码编译成机器代码这里的主要区别是V8不会产生字节码或任何中间代码。

V8曾经有两个编译器

在V8版本5.9之前发布(今年早些时候发布),引擎使用两个编译器:

  • full-codegen - 一个简单而非常快速的编译器,可以生成简单而相对较慢的机器代码。
  • 曲轴 - 更复杂(即时)优化编译器,可以生成高度优化的代码。

V8引擎还内部使用多个线程:

  • 主线程会执行你所期望的:获取代码,编译然后执行它
  • 还有一个单独的线程用于编译,因此主线程可以在前者优化代码时继续执行
  • Profiler线程将告诉运行时间,我们花费大量时间的方法,以便曲轴可以优化它们
  • 几个线程来处理垃圾收集器扫描

当第一次执行JavaScript代码时,V8利用代码代码,它将被解析的JavaScript直接转换为机器代码,而无需任何转换。这使得它能够非常快速地开始执行机器代码请注意,V8不会以这种方式使用中间字节码表示,从而无需解释器。

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

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

内联

第一个优化是提前编写尽可能多的代码。内联是将被调用函数的正文替换为调用位置(函数所在的代码行)的过程。这个简单的步骤允许以下优化更有意义。

 

隐藏课

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

大多数JavaScript解释器都使用类似字典的结构(基于哈希函数)来将对象属性值的位置存储在内存中。这种结构使得检索JavaScript中的属性的值比在Java或C#这样的非动态编程语言中更昂贵。在Java中,所有对象属性都是由编译前的固定对象布局确定的,并且不能在运行时动态添加或删除(也就是说,C#具有另一个主题动态类型)。因此,属性(或指向这些属性的指针)的值可以作为连续缓冲区存储在存储器中,每个之间具有固定偏移量。偏移量的长度可以根据属性类型轻松确定,而在JavaScript中,属性类型在运行时可能会发生变化,这是不可能的。

由于使用字典来查找对象属性在内存中的位置是非常低效的,所以V8使用不同的方法:隐藏类隐藏类的工作方式类似于Java语言中使用的固定对象布局(类),除了它们在运行时创建。现在,我们来看看它们的实际情况:

函数Point(x,y){ 
    this.x = x; 
    this.y = y; 
}
var p1 = new Point(1,2);

一旦发生“新的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”。

 

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

函数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”的,然后再分配“一”。因此,由于不同的转换路径,“P1”和“P2”最终会有不同的隐藏类。在这种情况下,以相同的顺序初始化动态属性要更好,以便可以重用这些隐藏的类。

内联缓存

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

我们将触及内联缓存的一般概念(如果您没有时间通过​​上面的深入解释)。

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

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

内联缓存也是为什么同一类型的对象共享隐藏类非常重要的原因。如果您创建两个相同类型的对象和不同的隐藏类(如前面的示例所示),则V8将无法使用内联缓存,因为即使两个对象的类型相同,它们对应的隐藏类为其属性分配不同的偏移量。

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

编译到机器代码

一旦氢图被优化,曲轴将其降低到称为锂的较低级别表示。大多数锂实现都是架构特定的。注册分配发生在此级别。

最后将锂编成机器代码。然后发生其他事情,称为OSR:堆栈替换。在我们开始编译和优化一个显然长期运行的方法之前,我们可能会运行它。V8不会忘记它刚刚慢慢执行,再次使用优化版本。相反,它会转换所有上下文(堆栈,寄存器),以便在执行过程中切换到优化版本。这是一个非常复杂的任务,请记住,除了其他优化之外,V8最初已经编写了代码。V8不是唯一能够做到这一点的引擎。

有一种称为去优化的保护措施来做出相反的转换,并回到未优化的代码,以防发动机的假设不再成立。

垃圾收集

对于垃圾收集,V8采用传统的代数扫描方法来清理老一代。标记阶段应该停止执行JavaScript。为了控制GC成本并使执行更加稳定,V8使用增量式标记:而不是走整个堆,尝试标记每一个可能的对象,它只能走一部分堆,然后恢复正常执行。下一个GC停止将继续从之前的堆步行停止。这在正常执行期间允许非常短的暂停。如前所述,扫描阶段由单独的线程处理。

点火和涡轮风扇

随着V8 5.9的发布,2017年早些时候推出了一个新的执行流程。这个新的管道可以在现实的 JavaScript应用程序中实现更大的性能改进和显着的内存节省

新的执行管道建立在Ignition V8的解释器和V8的最新优化编译器TurboFan之上

自V8的5.9版本出来以来,V8团队一直努力跟上新的JavaScript语言功能,V8团队一直努力追求全新的代码和曲轴(自2010年起已经为V8提供的技术)已经不再被V8用于JavaScript执行这些功能所需的优化。

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

 
Web和Node.js基准测试的改进

这些改进只是开始。新的点火和涡轮风扇管道为进一步的优化铺平了道路,这将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占地面积。

最后,这里有一些关于如何编写优化,更好的JavaScript的提示和技巧。您可以从上面的内容中轻松获得这些内容,但是,为了方便起见,这里有一个摘要:

如何编写优化的JavaScript

  1. 对象属性的顺序:始终以相同的顺序实例化对象属性,以便可以共享隐藏类和随后优化的代码。
  2. 动态属性:在实例化后向对象添加属性将强制隐藏类更改,并减缓任何为先前隐藏类优化的方法。而是在其构造函数中分配对象的所有属性。
  3. 方法:重复执行相同方法的代码将运行速度快于执行许多不同方法的代码(由于内联缓存)。
  4. 数组:避免密钥不是增量数字的稀疏数组。其中没有每个元素的稀疏数组是一个哈希表这种阵列中的元素更容易访问。另外,尽量避免预先分配大数组。最好随着你的发展而增长。最后,不要删除数组中的元素。它使密钥稀疏。
  5. 标记值:V8表示32位的对象和数字。它使用一个位来知道它是否是一个对象(标志= 1)或称为SMI(SMall Integer)的整数(标志= 0),因为它是31位。然后,如果一个数值大于31位,V8将会把数字加到一个双数位,并创建一个新的对象来放入数字。尽可能使用31位有符号数字,以避免将昂贵的拳击操作转换成JS对象。
  6. 广告: 做网站 就上重庆星期天 专业的重庆网站建设公司

我们在SessionStack尝试遵循这些最佳做法来编写高度优化的JavaScript代码。原因是一旦将SessionStack集成到生产网络应用程序中,它就开始记录所有内容:所有DOM更改,用户交互,JavaScript异常,堆栈跟踪,失败的网络请求和调试消息。 
使用SessionStack,您可以将Web应用中的问题重播为视频,并查看用户发生的一切。所有这一切都必须发生,没有对您的网络应用程序的性能影响。

服务支持

我们珍惜您每一次在线询盘,有问必答,用专业的态度,贴心的服务。

让您真正感受到我们的与众不同!

合作流程
合作流程

重庆网站建设流程从提出需求到网站建设报价,再到网站建设,每一步都是规范和专业的。

常见问题
常见问题

什么是网站定制?网站报价如何?网站常见问题。

常见问题
售后保障

网站建设不难,难的是一如既往的热情服务及技术支持。我们知道:做网站就是做服务,就是做售后。