2017-10-19 09:17
该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。这第二篇文章将潜入Google V8 JavaScript引擎的内部部分。我们还将提供一些关于如何在构建产品时如何在SessionStack中编写更好的JavaScript代码实践我们的开发团队的快速提示。
甲JavaScript引擎是一个程序或执行JavaScript代码的解释器。JavaScript引擎可以实现为标准解释器,也可以以某种形式将JavaScript编译为字节码的即时编译器。
这是一个正在实施JavaScript引擎的热门项目列表:
由Google构建的V8引擎是开源的,用C ++编写。该引擎在Google Chrome中使用。然而,与其他引擎不同,V8也用于流行的Node.js运行时。
V8首先旨在提高Web浏览器中JavaScript执行的性能。为了获得速度,V8将JavaScript代码转换为更有效的机器代码,而不是使用解释器。它通过实施像许多现代JavaScript引擎(如SpiderMonkey或Rhino(Mozilla))的JIT(即时)编译器,将JavaScript代码编译成机器代码。这里的主要区别是V8不会产生字节码或任何中间代码。
在V8版本5.9之前发布(今年早些时候发布),引擎使用两个编译器:
V8引擎还内部使用多个线程:
当第一次执行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将无法使用内联缓存,因为即使两个对象的类型相同,它们对应的隐藏类为其属性分配不同的偏移量。
一旦氢图被优化,曲轴将其降低到称为锂的较低级别表示。大多数锂实现都是架构特定的。注册分配发生在此级别。
最后将锂编成机器代码。然后发生其他事情,称为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将会有更简单和更易维护的架构。
这些改进只是开始。新的点火和涡轮风扇管道为进一步的优化铺平了道路,这将在未来几年内提升JavaScript性能并缩小V8在Chrome和Node.js中的占地面积。
最后,这里有一些关于如何编写优化,更好的JavaScript的提示和技巧。您可以从上面的内容中轻松获得这些内容,但是,为了方便起见,这里有一个摘要:
我们在SessionStack尝试遵循这些最佳做法来编写高度优化的JavaScript代码。原因是一旦将SessionStack集成到生产网络应用程序中,它就开始记录所有内容:所有DOM更改,用户交互,JavaScript异常,堆栈跟踪,失败的网络请求和调试消息。
使用SessionStack,您可以将Web应用中的问题重播为视频,并查看用户发生的一切。所有这一切都必须发生,没有对您的网络应用程序的性能影响。