当前位置: 首页 > Web前端 > HTML

JavaScript是如何工作的:深入V8引擎&编写优化代码的5个tips

时间:2023-04-02 19:32:38 HTML

阿里云最近在做活动,低至20折,有兴趣的可以看看:https://promotion.aliyun.com/...为了保证更好的可读性,本文采用意译而非直译。本系列的第一篇文章重点介绍了引擎、运行时和调用堆栈。第二篇文章将深入探讨Google的V8JavaScript引擎的内部结构。想阅读更多优质文章,请戳GitHub博客,一年百篇优质文章等你来!概述JavaScript引擎是执行JavaScript代码的程序或解释器。JavaScript引擎可以实现为标准解释器,或某种形式的将JavaScript编译为字节码的即时编译器。实现JavaScript引擎的热门项目列表:V8——开源,由Google开发,用C++编写Rhino——由Mozilla基金会管理,开源,完全用Java开发SpiderMonkey——是第一个支持NetscapeNavigator的JavaScript引擎,目前被FirefoxJavaScriptCore使用——开源,作为Nitro出售,由Apple为Safari开发KJS——KDE的引擎,最初由HarriPorten为KDE项目中的Konqueror网络浏览器开发Chakra(JScript9)——InternetExplorerChakra(JavaScript)—MicrosoftEdgeNashorn,OpenJDK的一部分,由Oracle的Java语言和工具组编写JerryScript—物联网的轻量级引擎为什么要创建V8引擎?Google构建的V8引擎是开源的,用C++编写。该引擎用于GoogleChrome,但与其他引擎不同的是,V8还用于流行的node.js。V8最初旨在提高JavaScript在Web浏览器中的执行性能。为了提高速度,V8将JavaScript代码转换为更高效的机器代码,而不是使用解释器。它通过实现JIT(即时)编译器将JavaScript代码编译为运行时机器代码,就像SpiderMonkey或Rhino(Mozilla)等许多现代JavaScript引擎所做的那样。这里的主要区别是V8不生成字节码或任何中间代码。V8曾经有两个编译器在V85.9版本出来之前,V8引擎使用了两个编译器:full-codegen—一个简单且速度非常快的编译器,可以生成简单且相对较慢的机器码。Crankshaft—一种更复杂的(即时)优化编译器,可生成高度优化的代码。V8引擎内部也使用多线程:主线程做你想做的事情:获取代码,编译代码,执行它还有一个单独的编译线程,所以主线程可以继续执行一个,而前者优化代码Profiler线程,它会告诉运行时我们花了很多时间,以便Crankshaft可以优化它们有些线程处理垃圾收集器当JavaScript代码第一次执行时,V8使用一个完整的代码生成编译器,它翻译将JavaScript直接解析为机器代码,无需任何转换。这允许它非常快速地开始执行机器代码。请注意,V8不使用中间字节码,因此不需要解释器。当代码运行一段时间后,分析线程已经收集到足够的数据来确定应该优化哪个方法。接下来,Crankshaft从另一个线程开始优化。它将JavaScript抽象语法树转换为称为Hydrogen的高级静态单一赋值(SSA)表示,并尝试优化Hydrogen图,大多数优化都是在这个级别完成的。内联代码第一个优化是提前内联尽可能多的代码。内联是用被调用函数的主体替换调用点(调用函数的代码行)的过程。这个简单的步骤让后面的优化变得更有意义。隐藏类JavaScript是一种基于原型的语言:类和对象不是使用克隆过程创建的。JavaScript也是一种动态编程语言,这意味着可以在实例化后轻松地向对象添加或删除属性。大多数JavaScript解释器使用类似字典的结构(基于哈希函数)来存储对象属性值在内存中的位置,这使得在JavaScript中检索属性值比在非动态编程语言(如Java)中容易得多或C#的计算成本更高。在Java中,所有的对象属性在编译前都是由一个固定的对象布局决定的,不能在运行时动态添加或删除(当然C#有动态类型,那是另外一个话题了)。因此,属性值(或指向这些属性的指针)可以作为连续的缓冲区存储在内存中,每个缓冲区之间有固定的偏移量,偏移量的长度可以很容易地根据属性类型确定,而在运行时这不是可能在JavaScript中更改属性类型。由于使用字典查找对象属性在内存中的位置效率非常低,因此V8使用了一种不同的方法:隐藏类。隐藏类的工作方式类似于Java等语言中使用的固定对象(类),只是它们是在运行时创建的。现在,让我们看看它们的实际应用:一旦“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”。隐藏类转换取决于将属性添加到对象的顺序。看看下面的代码片段:现在,假设对于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停止将从上一个堆遍历停止的地方继续,这允许在正常执行期间有非常短的暂停,如前所述,扫描阶段由一个单独的线程处理。如何编写JavaScript对象属性的优化顺序:对象属性总是以相同的顺序实例化,以便隐藏类和随后优化的代码可以共享。动态属性:因为在实例化后向对象添加属性将强制执行隐藏类更改并减慢先前由隐藏类优化的所有方法的执行,所以在其构造函数中分配对象的所有属性。方法:重复执行相同方法的代码将比只执行一次多个不同方法的代码运行得更快(由于内联缓存)。数组:避免使用稀疏数组,其中键不是自增数,不存储所有元素的稀疏数组是哈希表。这样的数组中的元素访问是昂贵的。另外,尽量避免预分配大数组。最好按需增长。最后,不要删除数组中的元素,这样会使键值稀疏。标记值:V8使用32位来表示对象和值。由于该值是31位,所以它使用一位来区分它是一个对象(flag=1)还是一个称为SMI(SMallInteger)的整数(flag=0)。所以,如果一个数字大于31位,V8会把这个数字装箱,把它变成一个双精度数,并创建一个新的对象来保存这个数字。尽可能使用31位有符号数以避免昂贵的JS对象装箱。Ignition和TurboFan在2017年初发布的V85.9中引入了新的执行管道。这个新管道在实际JavaScript应用程序中实现了更大的性能提升和显着的内存节省。新的执行流程建立在Ignition(V8的解释器)和TurboFan(V8最新的优化编译器)之上。自V8版本5.9以来,V8团队已经不再使用full-codegen和Crankshaft(自2010年起使用V8技术),因为V8团队努力跟上新的JavaScript语言特性和这些特性所需的优化。这意味着V8将拥有一个更简单、更易于维护的整体架构。这些改进仅仅是个开始。新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提高JavaScript性能并缩小V8在Chrome和Node.js中的占用空间。原文:https://blog.sessionstack.com...你的点赞是我继续分享好东西的动力,欢迎点赞!干货交流系列文章总结如下。我觉得点个Star就好了。欢迎加群,互相学习。https://github.com/qq44924588...我是小智,公众号《大招天下》的作者,前端技术爱好者。我会经常分享自己学习看到的干货,在进步的路上互相鼓励!关注公众号,后台回复福利,就能看到福利,你懂的。