JavaScript的工作方式:优化V8编译器以提高效率

了解Javascript的工作方式是编写高效Javascript的关键。忘记那些无关紧要的毫秒级改进:错误地使用对象属性可能导致简单的一行代码速度降低7倍。

考虑到javascript在软件堆栈的所有级别(la mean或replacements 1、2、3,)中的普遍性,即使不是所有级别的基础设施,也可能会出现微不足道的减速,而不仅仅是网站的菜单动画。

考虑到Javascript在软件栈的所有级别上都无处不在(就像均值或替换1、2、3那样),即使不是所有级别的基础设施都受到无关紧要的减速的困扰——不仅仅是网站的菜单动画。

有许多方法可以编写更高效的Javascript,但在本文中,我们将重点讨论对编译器友好的Javascript优化方法,这意味着源代码使编译器优化更容易和有效。

我们将把讨论范围缩小到v8,即支持electron、node.js和google chrome的javascript引擎。为了理解编译器友好的优化,我们首先需要讨论如何编译javascript。

V8中的Javascript执行分为三个阶段:

  • 源代码到语法树:解析器从源代码生成一个抽象语法树(AST)

  • 语法树到字节码:V8的解释器Ignition从语法树生成字节码。 请注意,此字节码步骤在2017年之前不存在。此处介绍了2017年之前的V8。

  • 字节码到机器码:V8的编译器TurboFan从字节码生成一个图,用高度优化的机器码替换字节码的部分

第一个阶段超出了本文的范围,但是第二个和第三个阶段对编写优化的javascript有直接的影响。

我们将讨论这些优化方法以及您的代码如何利用(或滥用)这些优化。 通过了解Javascript执行的基础知识,您不仅会了解这些性能建议,而且还将学习如何发现自己的一些建议。

实际上,第二阶段和第三阶段是紧密相连的。这两个阶段在准时制(JIT)范例中运作。为了理解JIT的重要性,我们将研究以前将源代码转换成机器码的方法。

这是一种很好的模式

要执行任何程序,计算机必须将源代码翻译成机器可以运行的机器语言。

有两种方法可以完成此翻译。

第一种选择涉及使用解释器。 解释器有效地逐行翻译和执行。

第二种方法是使用编译器。 编译器在执行之前立即将所有源代码翻译成机器语言。 鉴于以下所述的优点和缺点,每种方法都有其位置。

翻译的利弊

解释器使用读取评估打印循环(REPL)进行操作-此方法具有许多有利的特性:

  • 易于实施和理解

  • 即时反馈

  • 更舒适的编程环境

但是,这些好处是以执行缓慢为代价的,因为(1)与运行机器码相比,eval的开销较大,而且(2)不能跨程序的各个部分进行优化。

更正式地说,解释器在处理不同的代码段时无法识别重复的工作。 如果您通过解释程序运行同一行代码100次,则解释器将翻译并执行同一行代码100次-不必要地重新执行99次。

总之,解释器启动简单、快速,但执行缓慢。

编译器的利弊

相比之下,编译器在执行之前一次翻译所有源代码。

随着复杂性的增加,编译器可以进行全局优化(例如,对于重复的代码行共享机器码)。这为编译器提供了它们相对于解释器的唯一优势——更快的执行时间。

本质上,编译器很复杂,启动慢,但执行快。

即时编译

即时编译器试图结合解释器和编译器的最佳部分,从而使翻译和执行都非常快。

基本思想是尽可能避免重新翻译。 首先,分析器只需通过解释器运行代码即可。 在执行期间,事件探查器跟踪运行几次的热代码段和运行很多次的热代码段。

JIT将热代码段发送给基准编译器,并在可能的情况下重用已编译的代码。

jit还将热代码段发送给优化编译器。此编译器使用解释器收集的信息来(a)进行假设,(b)基于这些假设进行优化(例如,对象属性总是以特定的顺序出现)。

但是,如果这些假设无效,优化编译器将执行反优化,这意味着它将丢弃优化后的代码。

优化和反优化周期是昂贵的,并导致了下面详细描述的一类Javascript优化方法。

JIT还引入了与存储优化的机器代码和分析器的执行信息相关的开销内存成本。 尽管优化的Javascript无法改善这一点,但是这种内存开销会激发V8解释器Ignition。

V8编译

V8的Ignition和TurboFan执行以下功能:

Ignition将AST转换为字节码。 然后执行字节码序列,并通过内联高速缓存收集反馈。 Ignition本身用于随后的解释,TurboFan用于推测性优化,均使用此反馈。

基于反馈,TurboFan通过将字节码转换成特定于体系结构的机器码来优化字节码。

Ignition

JIT编译器显示出开销内存消耗。 点火通过实现三个目标(幻灯片)来解决此问题:减少内存使用量,减少启动时间以及降低复杂性。

这三个目标都是通过将ast编译为字节码并在程序执行期间收集反馈来实现的。

  • 这个字节码用作事实的来源,消除了编译期间重新解析Javascript的需要。这意味着使用字节码,TurboFan的反优化不再需要原始源代码。

  • 作为基于程序执行反馈进行优化的示例,“内联缓存”使V8可以优化对具有相同类型参数的函数的重复调用。 具体地说,内联缓存存储函数输入的类型。 类型越少,所需的类型检查就越少。 减少类型检查的数量可以显着提高性能。

AST和字节码都暴露给TurboFan优化编译器。

TurboFan

在2008年发布时,V8引擎最初直接将源代码编译为机器代码,跳过了中间字节码表示。

但是,TurboFan现在接受Ignition的中间字节码,并且比2008年快10倍。同一主题演讲介绍了V8编译器的过去迭代及其失败之处:

2008 – Full-Codegen

  • 具有隐藏类(slides)、内联缓存(slides)的快速ast walking jit编译器

  • 缺点:非优化jit

2010 – Crankshaft

  • 使用类型反馈(slides)和去优化(slides)优化jit编译器

  • 缺点:无法扩展到现代Javascript,严重依赖于去优化,有限的静态类型分析,与Codegen紧密耦合,高移植开销

2015 – TurboFan

  • 用类型和范围分析优化jit编译器

TurboFan可以优化性能,静态类型信息的使用,编译器前端,中间和后端的分离以及可测试性。这最终导致了关键的贡献,称为节点海。

在节点的海洋中,节点代表计算,边缘代表依存关系。

与控制流图(CFG)不同,节点的海洋放松了大多数操作的计算顺序。与CFG类似,有状态操作的控制边和效果边在需要时约束执行顺序。

Titzer进一步将该定义细化为节点的集合,其中控制流子图得到了进一步的放松。 这提供了许多优点-例如,这避免了多余的代码消除。

通过自下而上或自上而下的图转换,图缩减被应用于这一系列节点。

TurboFan管道按照4个步骤将字节码转换成机器码。请注意,以下管道中的优化是根据Ignition收集的反馈信息进行的:

  • Express程序作为Javascript运算符(例如JSAdd)

  • Express程序作为中间运算符(VM级运算符;与数字表示形式无关,例如NumberAdd)

  • 作为机器操作员的快速程序(对应于机器指令,如int32add)

  • 使用顺序约束安排执行顺序。 创建一个传统的CFG。

TurboFan的在线jit风格的编译和优化完成了V8从源代码到机器码的转换。

如何优化您的Javascript

TurboFan的优化通过减轻糟糕的JavaScript的影响来提高JavaScript的网络性能。然而,了解这些优化可以提供进一步的加速。

下面是通过在V8中利用优化来提高性能的7个技巧。前四个重点是减少反优化。

1:在构造函数中声明对象属性

更改对象属性会产生新的隐藏类。以google i/o 2012中的以下示例为例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

var p1 = new Point(11, 22);  // hidden class Point created
var p2 = new Point(33, 44);

p1.z = 55;  // another hidden class Point created

如您所见,p1p2现在具有不同的隐藏类。 这阻碍了TurboFan进行优化的尝试:具体而言,现在接受Point对象的任何方法都已取消优化。

所有这些功能都使用两个隐藏类进行了重新优化。 对于对象形状的任何修改都是如此。

2:保持对象属性排序不变

更改对象属性的顺序会导致新的隐藏类,因为对象形状中包括了顺序。

const a1 = { a: 1 };  # hidden class a1 created
a1.b = 3;

const a2 = { b: 3 };  # different hidden class a2 created
a2.a = 1;

上面的a1和a2现在也具有不同的隐藏类。 固定顺序使编译器可以重用相同的隐藏类,因为添加的字段(包括顺序)用于生成隐藏类的ID。

3:修复函数参数类型

函数根据特定参数位置处的值类型更改对象形状。 如果此类型发生更改,则会对该功能进行反优化并重新对其进行优化。

看到四个不同的对象形状后,该功能将变为大形,因此TurboFan不会尝试优化该功能。

请看下面的例子。

function add(x, y) {
  return x + y
}

add(1, 2);  # monomorphic
add("a", "b");  # polymorphic
add(true, false);
add([], []);
add({}, {});  # megamorphic

L9之后,TurboFan将不再优化添加。

4:在脚本作用域中声明类

不要在函数作用域中定义类。以下面的例子说明这个病理病例:

function createPoint(x, y) {
  class Point {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
  }
  return new Point(x, y);
}

function length(point) {
  ...
}

每次调用函数createPoint时,都会创建一个新的Point原型。

每个新的原型对应于一个新的对象形状,因此长度函数可以在每个新点看到一个新的对象形状。

和以前一样,在看到4种不同的对象形状之后,该功能变为大形,并且TurboFan不会尝试优化长度。

通过将Point类放置在脚本作用域中,我们可以避免每次调用createPoint时都创建新的对象形状。

下一个技巧是V8引擎中的一个怪癖。

5:使用for ... in

这是V8发动机的一个怪癖,该功能已包含在原始曲轴中,后来又移植到了Ignition和Turbofan上。

for…in循环比for循环中的函数迭代,带有箭头函数的函数迭代和Object.keys快4-6倍。

以下是对由于现代V8引擎的改变而不再相关的前神话的两个反驳。

6:无关的字符不会影响性能

曲轴以前使用函数的字节数来确定是否内联函数。 但是,TurboFan建立在AST之上,并且使用AST节点数来确定函数大小。

结果,不相关的字符(例如空格,注释,变量名长度和函数签名)不会影响函数的性能。

7: Try/catch/finally is not ruinous

try块以前容易出现代价高昂的优化去优化周期。然而,今天,当从try块中调用函数时,turbofan不再显示出显著的性能影响。

结论

总而言之,优化方法通常集中在减少反优化和避免不可优化的大形函数上。

了解了V8引擎框架后,我们还可以推断出上面未列出的其他优化方法,并尽可能重用这些方法以利用内联。 您现在已经了解了Javascript编译及其对日常Java语言使用的影响。

以上就是JavaScript的工作方式:优化V8编译器以提高效率的详细内容,更多请关注0133技术站其它相关文章!

赞(0) 打赏
未经允许不得转载:0133技术站首页 » JavaScript 教程