几周前,我们开始了一系列旨在深入研究JavaScript及其实际工作原理的文章:我们认为通过了解JavaScript的构建块及其工作原理您将能够共同编写更好的代码和应用程??序。本系列的第一篇文章重点介绍了引擎、运行时和调用堆栈的概述。第二篇文章将深入探讨Google的V8JavaScript引擎的内部结构。我们还将提供一些关于如何编写更好的JavaScript代码的快速提示-我们的SessionStack开发团队在构建产品时遵循的最佳实践。概述JavaScript引擎是执行JavaScript代码的程序或解释器。JavaScript引擎可以充当标准解释器或即时编译器,它将JavaScript编译成某种形式的字节码。以下是实现JavaScript引擎的流行项目列表:V8—开源,由Google开发,用C++Rhino编写—由Mozilla基金会管理,开源,完全用Java开发SpiderMonkey—是第一个支持NetscapeNavigatorJavaScript引擎,Firefox目前使用的JavaScriptCore—开放源代码,由Apple为SafariKJS开发—KDE的引擎,最初由HarriPorten在KDE项目Chakra(JScript9)中为Konqueror网络浏览器开发—InternetExplorerChakra(JavaScript)—MicrosoftEdgeNashorn,部分OpenJDK,由Oracle的Java语言和工具组编写JerryScript—物联网的轻量级引擎为什么要创建V8引擎?由谷歌构建的V8引擎是开源的,用C++编写。该引擎用于谷歌浏览器。与其他引擎不同,流行的Node.js中也使用了V8。V8最初旨在提高JavaScript在Web浏览器中的执行性能。为了提高速度,V8将JavaScript代码翻译成更高效的机器代码,而不是使用解释器来翻译代码。它使用SpiderMonkey等JIT(即时)编译器或Rhino(Mozilla)等许多现代JavaScript引擎将JavaScript代码编译成机器代码。这里的主要区别是V8不生成字节码或任何中间代码。V8曾经有两个编译器在V85.9版本出来之前(今年早些时候发布),该引擎使用了两个编译器:full-codegen-一个简单且速度非常快的编译器,它生成简单且相对较慢的机器代码。Crankshaft-一种更复杂的(即时)优化编译器,可生成高度优化的代码。V8引擎内部也使用了多线程:主线程做你期望的事情:获取代码,编译它,然后执行它还有一个单独的编译线程,这样主线程可以在前者优化的同时继续执行代码一个Profilerthread,它会告诉运行时我们花了很多时间,这样Crankshaft可以优化它们一些线程处理垃圾收集器当JavaScript代码第一次执行时,V8使用full-codegen编译器,它直接将解析后的JavaScript翻译成机器码,无需任何转换。这允许它非常快速地开始执行机器代码。请注意,V8不使用中间字节码,因此不需要解释器。当您的代码运行了一段时间后,探查器线程已收集到足够的数据来确定应该优化哪个方法。接下来,Crankshaft从另一个线程开始优化。它将JavaScript抽象语法树转换为称为Hydrogen的高级静态单一赋值(SSA)表示,并尝试优化Hydrogen图。大多数优化都是在这个级别完成的。内联代码第一个优化是提前内联尽可能多的代码。内联是用调用点(调用函数的代码行)替换被调用函数体的过程。这个简单的步骤使以下优化更有意义。隐藏类JavaScript是一种基于原型的语言:没有类和对象,而是使用克隆创建的。JavaScript也是一种动态编程语言,这意味着可以在实例化后方便地向对象添加或删除属性。大多数JavaScript解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得在JavaScript中检索属性值的计算量比在Java或C#等非动态编程语言中要多得多。在Java中,所有的对象属性在编译前都由一个固定的对象决定,不能在运行时动态添加或删除(当然C#的动态类型是另外一个话题)。因此,属性的值(或指向这些属性的指针)可以作为连续的缓冲区存储在内存中,每个值之间有一个固定的偏移量。偏移量的长度可以很容易地根据属性类型确定,这在属性类型可以在运行时更改的JavaScript中是不可能的。由于使用字典查找对象属性在内存中的位置效率非常低,因此V8使用了一种不同的方法:隐藏类。隐藏类的工作方式类似于Java等语言中使用的固定对象(类),只是隐藏类是在运行时创建的。现在,让我们看看它们的实际应用:functionPoint(x,y){this.x=x;this.y=y;}varp1=newPoint(1,2);一旦“newPoint(1,2)”发生调用,V8将创建一个名为“C0”的隐藏类。没有为Point定义属性,因此“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”添加到Point对象(已经包含属性“x”),则将类转换添加到“C1”,隐藏类应该更改为“C2”,点对象的隐藏类更新为“C2”。隐藏类转换取决于将属性添加到对象的顺序。看看下面的代码片段:functionPoint(x,y){this.x=x;this.y=y;}varp1=newPoint(1,2);p1.a=5;p1.b=6;varp2=新点(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被编译为机器代码。然后是OSR:on-stackreplacement(堆栈替换)。我们可能会在开始编译和优化显式长时间运行的方法之前运行堆栈替换。V8不只是缓慢地执行堆栈替换并重新开始优化。相反,它转换我们拥有的所有上下文(堆栈、寄存器)以在执行期间切换到优化版本。这是一项非常复杂的任务,考虑到除其他优化外,V8最初内联代码。V8并不是唯一能够做到这一点的引擎。有一种称为反优化的保护措施可以进行相反的转换,并在假定引擎优化无效的情况下恢复为未优化的代码。垃圾收集对于垃圾收集,V8使用传统的分代清理来清理老年代。标记阶段应该停止JavaScript的执行。为了控制GC成本并使执行更稳定,V8使用渐进式标记:不是遍历整个堆内容,而是尝试标记每个可能的对象。它只遍历堆的一部分,然后恢复正常执行。下一次GC将从之前遍历堆的位置继续。这允许在正常执行期间有非常短的暂停。如前所述,扫描阶段由不同的线程处理。Ignition和TurboFan随着2017年初V85.9的发布,引入了新的执行管道。这个新管道可以在实际JavaScript应用程序中实现更大的性能改进和显着的内存节省。新的执行流程建立在Ignition(V8的解释器)和TurboFan(V8的最佳优化编译器)之上。您可以查看V8团队关于此主题的博文。自V85.9发布以来,V8团队已经不再使用full-codegen和Crankshaft(自2010年起使用V8技术),因为V8团队努力跟上新的JavaScript语言特性和这些特性所需的优化。这意味着V8将拥有一个更简单、更易于维护的整体架构。Web和Node.js性能的这些改进仅仅是个开始。新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提高JavaScript性能并缩小V8在Chrome和Node.js中的足迹。***,这里有一些关于如何编写优化的、更好的JavaScript的技巧。您可以轻松地从上面得到这一点,但为了您的方便,这里有一个摘要:如何编写优化的JavaScript对象属性顺序:始终以相同的顺序实例化对象属性,以便共享隐藏类和随后优化的代码。动态属性:在实例化后向对象添加属性将强制执行隐藏类更改并减慢先前由隐藏类优化的任何方法的执行。相反,在其构造函数中分配对象的所有属性。方法:重复执行相同方法的代码将比只执行一次多个不同方法的代码运行得更快(由于内联缓存)。数组:避免键不是自动递增数字的稀疏数组。不存储其所有元素的稀疏数组是哈希表。这样的数组中的元素访问是昂贵的。另外,尽量避免预分配大数组。***是按需增长。***,不删除数组中的元素。这使得键值稀疏。标记值:V8使用32位来表示对象和值。由于该值是31位,所以它使用一位来区分它是一个对象(flag=1)还是一个称为SMI(SMallInteger)的整数(flag=0)。所以,如果一个数字大于31位,V8会把这个数字装箱,把它变成一个双精度数,并创建一个新的对象来保存这个数字。尽可能使用31位有符号数以避免昂贵的JS对象装箱。当我们尝试在SessionStack中编写高度优化的JavaScript代码时,我们遵循这些最佳实践。原因是,一旦将SessionStack集成到生产级Web应用程序中,它就会开始记录所有内容:所有DOM更改、用户交互、JavaScript异常、堆栈跟踪、网络请求失败、调试消息等。使用SessionStack,您可以重现问题作为视频并查看发生在用户身上的一切。所有这些都必须在不影响Web应用程序性能的情况下完成。有一个免费的计划,所以你可以试一试。
