几个星期前,我们开始了一系列旨在深入挖掘 JavaScript 及其工作原理的系列:通过了解JavaScript的构建模块以及它们如何共同发挥作用,你将能够编写更好的代码和应用程序。 本系列的第一篇文章重点介绍了引擎,运行时和调用堆栈的概述。 第二篇文章将深入探讨谷歌V8 JavaScript引擎的内部原理。 我们还将提供一些关于如何编写更好的JavaScript代码的快速提示: 我们的SessionStack开发团队在构建产品时所遵循的最佳实践。 概览JavaScript 引擎是执行 JavaScript 代码的程序或解释器。 JavaScript 引擎可以实现为标准解释器或即时编译器,它以某种形式将 JavaScript 编译为字节码。 下面是一个JavaScript引擎实现的热门项目列表:
为什么要开发V8引擎?由谷歌开发的V8引擎是用C ++编写开源软件。 此引擎在Google Chrome中使用。 但是,与其他引擎不同的是,流行的Node.js也把V8也作为运行时环境使用。 V8最初是为了提高Web浏览器中 JavaScript 执行的性能。 为了提高运行速度,V8 将 JavaScript 代码转换为更高效的机器代码,而不是使用解释器运行。 它通过实现JIT(即时)编译器将 JavaScript 代码编译成机器代码,这一点与许多现代 JavaScript 引擎一样,如 SpiderMonkey 或 Rhino(Mozilla)。 不过主要区别是V8不产生字节码或任何中间代码。 V8 曾经有两个编译器在 V8 的 5.9 版本出现之前(2017年上半年发布),该引擎使用了两个编译器:
V8引擎还在内部使用多个线程:
当首次执行 JavaScript 代码时,V8 会用 full-codegen直接将解析后的 JavaScript 代码转换为机器代码而无需其它转换。这使得它可以马上开始执行机器代码。 请注意:V8不使用中间字节码表示,因此无需解释器。 当代码运行一段时间之后,分析器线程已经收集到了足够的数据,知道了应该优化哪个方法。 接下来,Crankshaft优化从另一个线程开始。 它将 JavaScript 抽象语法树转换成名为 Hydrogen的高级静态单分配(SSA:static single-assignment)表示,并尝试优化 Hydrogen graph。 大多数优化都是在这个级别完成的。 内联第一个优化是提前内联尽可能多的代码。 内联是用被调函数的函数体替换调用点(调用函数的代码行)的过程。 这个简单的步骤使后面的优化更有意义。 隐藏类JavaScript是一种基于原型的语言:没有类,使用克隆过程创建对象。 JavaScript也是一种动态编程语言,这意味着可以在实例化后可以轻松地在对象中添加或删除属性。 大多数JavaScript解释器使用类似字典的结构(基于散列函数)在内存中存储对象属性值。 这种结构使得在JavaScript中检索属性值的计算成本比在 Java 或 C# 等非动态编程语言中更高。 在Java中,所有对象属性都是在编译之前由固定对象布局确定的,并且无法在运行时动态添加或删除(好吧,C# 具有动态类型,不过这是另一个话题)。 这样一来,属性值(或指向这些属性的指针)可以作为连续缓冲区存储在存储器中,每个缓冲区之间具有固定偏移量,可以根据属性类型轻松确定偏移的长度。而对于在运行时可以更改属性类型的 JavaScript,这是不可能做到的。 由于使用字典查找对象属性在内存中的位置效率非常低,因此V8使用不同的方法:隐藏类。 隐藏类的工作方式类似于 Java 等语言中使用的固定对象布局(类),除非它们是在运行时创建的。 现在,让我们看看它们实际上是什么样的: 一旦 此时尚未为Point定义任何属性,因此 一旦第一个语句 每次将新属性添加到对象时,旧的隐藏类都会更新为指向新隐藏类的转换路径。 隐藏类转换非常重要,因为它们允许在以相同方式创建的对象之间共享隐藏类。 如果两个对象共享一个隐藏类,并且同一属性被添加到它们之中,那么转换将确保两个对象都能够接收到相同的新隐藏类和随之附带的所有优化代码。 在执行语句 创建一个名为 隐藏类的转换取决于属性添加到对象的顺序。 看下面的代码片段: 看到上面的代码,你会认为对于p1和p2,将使用相同的隐藏类和转换。 实际上不是这样的。 对于 内联缓存V8 还使用了另一种技术来优化动态类型语言,被称为内联缓存。 内联缓存依赖于观察到的一种现象,那就是相同方法总是会被同一类型的对象的重复调用。 可以在这里找到对内联缓存的深入解释 (https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches)。 下面我们将讨论内联缓存的一般概念(如果你没有时间仔细阅读上面的深入解释的话)。 那么它是怎样工作的呢? V8 维护一个在最近的方法调用中作为参数传递的对象类型的缓存,并以此信息来推测将来作为参数传递的对象类型。如果V8能够正确的推测出对传递给方法的对象类型,那么它就可以跳过确定如何访问对象属性的这一个步骤,这样就可以使用之前查找过的信息确定对象的隐藏类。 那么隐藏类和内联缓存这两个概念的关联是什么呢?每当在特定对象上调用方法时,V8 引擎必须找到该对象的隐藏类,才能确定访问特定属性的偏移量。当同一方法两次成功调用到同一个隐藏类之后,V8会省略对隐藏类的查找,直接将属性的偏移量添加到对象指针本身。对于该方法的所有将来的调用,V8引擎假设隐藏类并未更改,并且使用之前查找到并存储的偏移量直接跳转到特定属性的内存地址。这就大大提高了执行速度。 内联缓存也是相同类型的对象共享隐藏类的重要原因。如果你要创建两个类型相同但是隐藏类不同的对象(正如我们之前的例子中所做的那样)的话,V8将无法使用内联缓存,因为即使这两个对象属于同一类型,但是它们相对应的隐藏类为其属性分配的偏移量很有可能是不同的。 a 和b 两个属性是按照不同顺序创建的。这两个对象基本相同,但 编译为机器代码Hydrogen graph优化后,Crankshaft 会将其降低到被称为 Lithium 的低级别表示。大多数 Lithium 实现都是特定于体系结构的。寄存器分配发生在这一级别。 最后,Lithium 被编译成机器代码。然后发生了一些被称为 OSR 的事:栈替换(on-stack replacement)。当一个显然会长时间运行的方法在我们开始编译和优化之前,它可能已经在运行。 V8 在重新启动优化版本之前并会任由这些代码缓慢的执行。相反,它将转换我们拥有的所有上下文(堆栈,寄存器),以便可以在执行过程中切换到优化版本。这是一项非常复杂的任务,考虑到其他优化,V8在一开始就已经内联了代码。 V8并不是唯一能够做到这一点的引擎。 有一种被称为去优化的保护措施可以进行相反的转换,如果引擎作出的假设不再成立,则恢复到非优化代码。 垃圾收集对于垃圾收集,V8采用传统的标记和扫描方式来清理老生代。标记阶段应该停止JavaScript执行。为了控制GC成本并使执行更加稳定,V8使用了增量标记:不是遍历整个堆的同时尝试标记每个可能的对象,它只是遍历堆的一部分,然后恢复正常执行。 下一次GC将从上一次堆遍历停止的位置继续。这样会在正常执行期间只有非常短暂的暂停。 如前文所述,扫描阶段由单独的线程进行处理。 Ignition and TurboFan2017年早些时候发布的V8 5.9,引入了新的执行管道。 事实证明,这个新的管道实现了更高的性能提升,并显著的节省了内存开销。 新的执行管道建立在 Ignition (https://github.com/v8/v8/wiki/Interpreter)、V8的解释器和TurboFan(V8的最新优化编译器)之上。 你可以查看V8团队关于该主题的博客文章 (https://v8project./2017/05/launching-ignition-and-turbofan.html)。 自从V8的 5.9 版本问世以来,V8已经不再使用 full-codegen 和 Crankshaft(自2010年以来为V8提供服务的技术)用于JavaScript执行,因为V8团队一直在努力跟上新的JavaScript语言功能,并且这些功能需要优化。 这意味着整体V8将会具有更简单,更易维护的架构。 这些改进只是一个开始。 新的Ignition和TurboFan管道为进一步优化铺平了道路,这些优化将在未来几年内提升JavaScript性能,并减少V8在Chrome和Node.js中所占用的空间。 最后,有一些关于如何编写良好优化的JavaScript的技巧和窍门。 你可以从上面的内容轻松地推导出这些内容,下面是一个简要的总结: 如何编写优化的JavaScript代码
我们在为 SessionStack 编写高度优化的 JavaScript 代码时一直遵循这些最佳实践。 原因是一旦把 SessionStack 集成到Web应用的生产环境中,它就会开始记录所有内容:所有DOM更改、用户交互、JavaScript异常、堆栈跟踪、失败的网络请求和调试消息。 通过SessionStack,你可以将网络应用中的问题重现,并查看发生的所有事情,同时对你的Web应用没有性能影响。 有一个免费的工具,不需要支付任何费用。 现在就可以试试(https://www./solutions/developers/?utm_source=medium&utm_medium=blog&utm_content=Post-1-overview-getStarted)。 |
|
来自: 西北望msm66g9f > 《编程》