V8 中的 JavaScript 性能提示

Chris Wilson
Chris Wilson

简介

Daniel Clifford 在 Google I/O 大会上发表了精彩演讲,介绍了提高 V8 中的 JavaScript 性能的提示和技巧。Daniel 鼓励我们“更快地满足需求”,仔细分析 C++ 和 JavaScript 之间的性能差异,并在编写代码时仔细考虑 JavaScript 的工作原理。本文总结了小尼的演讲中最重要的要点。当效果指南发生变化时,我们还会更新本文。

最重要的建议

请务必将效果提升建议结合起来考虑。绩效建议会让人上瘾,而有时先着眼于深入的建议可能会分散人们对实际问题的注意力。您需要全面了解自己的 Web 应用的性能。在关注这些性能提示之前,您应该使用 PageSpeed 等工具分析代码,然后提高得分。这有助于避免过早优化。

如需在 Web 应用中获得良好性能,最佳基本建议是:

  • 在出现问题(或发现)问题之前做好准备
  • 然后,确定并了解问题的根源
  • 最后,修正重要问题

为了完成这些步骤,了解 V8 如何优化 JS 很重要,这样您在编写代码时就可以考虑到 JS 运行时设计。此外,了解可用的工具以及这些工具如何为您提供帮助也很重要。Daniel 在他的演讲中进一步说明了如何使用开发者工具;本文档只记录了 V8 引擎设计的一些要点。

接下来,让我们了解一下 V8 的使用技巧!

隐藏的类

JavaScript 的编译时类型信息有限:类型可以在运行时更改,因此,在编译时推断 JS 类型的开销很明显。这可能导致您质疑 JavaScript 性能如何接近 C++。但是,V8 具有在运行时为对象内部创建的隐藏类型;然后,具有相同隐藏类的对象可以使用同一经优化的生成代码。

例如:

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

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```

在对象实例 p2 添加其他成员“.z”之前,内部 p1 和 p2 拥有相同的隐藏类,因此 V8 可以为操纵 p1 或 p2 的 JavaScript 代码生成优化的汇编版本。您避免的类别差异越大,效果就越好。

因此

  • 初始化构造函数函数中的所有对象成员(这样实例以后不会更改类型)
  • 始终以相同的顺序初始化对象成员

Numbers 表格

当类型发生变化时,V8 使用标记来高效表示值。V8 会根据您使用的数字类型进行推断。一旦 V8 进行了这种推断,它就会使用标记高效地表示值,因为这些类型可以动态变化。但是,更改这些类型标记有时会产生开销,因此最好始终使用数字类型,而且通常最好在适当的情况下使用 31 位有符号整数。

例如:

var i = 42;  // this is a 31-bit signed integer
var j = 4.2;  // this is a double-precision floating point number```

因此

  • 首选可以表示为 31 位带符号整数的数值。

数组

为了处理大型和稀疏数组,内部有两种数组存储:

  • 快速元素:用于紧凑键集的线性存储
  • 字典元素:哈希表存储,否则

最好不要使数组存储从一种类型切换为另一种类型。

因此

  • 为数组使用从 0 开始的连续键
  • 不要将大型数组(例如超过 64K 的元素)预先分配为其最大大小,而应随着您的容量增加
  • 请勿删除数组中的元素,尤其是数字数组
  • 不要加载未初始化或已删除的元素:
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // Much better! 2x faster.
}

此外,双精度浮点型的数组速度更快,数组的隐藏类会跟踪元素类型,而仅包含双精度型数组的数组会取消装箱(这会导致隐藏类发生变化)。但是,由于装箱和开箱,不小心对数组进行操作可能会造成额外的工作,例如

var a = new Array();
a[0] = 77;   // Allocates
a[1] = 88;
a[2] = 0.5;   // Allocates, converts
a[3] = true; // Allocates, converts```

效率不及:

var a = [77, 88, 0.5, true];

因为在第一个示例中,各个赋值会依次执行,并且 a[2] 的赋值会导致 Array 转换为未开箱的双精度数组,而 a[3] 的赋值则会将其重新转换回可包含任何值(数字或对象)的数组。在第二种情况下,编译器知道字面量中所有元素的类型,可以预先确定隐藏类。

  • 使用数组字面量对固定大小的小型数组进行初始化
  • 在使用之前预分配小数组(小于 64k)以更正大小
  • 不要将非数字值(对象)存储在数字数组中
  • 如果在不使用字面量的情况下进行初始化,请注意不要导致小型数组重新进行转换。

JavaScript 编译

虽然 JavaScript 是一种非常动态的语言,其原始实现是解释器,但现代 JavaScript 运行时引擎使用的是编译。V8(Chrome 的 JavaScript)提供了两种不同的即时 (JIT) 编译器,事实上:

  • “Full”编译器,可为任何 JavaScript 生成良好的代码
  • 优化编译器,可为大多数 JavaScript 生成出色的代码,但需要更长的编译时间。

完整编译器

在 V8 中,完整编译器会针对所有代码运行,并尽快开始执行代码,从而快速生成优质但不太出色的代码。此编译器在编译时几乎不会对类型做出假设,它预计变量类型可以而且在运行时会发生变化。完全编译器生成的代码使用内联缓存 (IC) 在程序运行时优化有关类型的知识,从而提高即时效率。

内嵌缓存的目标是通过为操作缓存与类型相关的代码来高效处理类型;当代码运行时,它会先验证类型假设,然后使用内嵌缓存来简化操作。但是,这意味着接受多种类型的操作的性能会降低。

因此

  • 首选单态运算而非多态运算

如果输入的隐藏类始终相同,则操作是单态的;否则,操作是多态的,这意味着某些参数可以在操作的不同调用中改变类型。例如,此示例中的第二个 add() 调用会导致多态性:

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

add(1, 2);      // + in add is monomorphic
add("a", "b");  // + in add becomes polymorphic```

优化编译器

与完整编译器并行使用,V8 使用优化编译器重新编译“热”函数(即运行多次的函数)。该编译器使用类型反馈来加快编译后的代码速度 - 事实上,它使用了取自我们刚刚讨论的 IC 的类型!

在优化编译器中,运算会推测性地内联(直接放在调用位置)。这样可以加快执行速度(以占用内存为代价),但也可以实现其他优化。单态函数和构造函数可以完全内联(这也是在 V8 中适合采用单态的另一个原因)。

您可以记录使用独立“d8”版 V8 引擎进行优化的内容:

d8 --trace-opt primes.js

(这会将优化函数的名称记录到 stdout 中)。

然而,并非所有函数都可以优化,某些功能会阻止优化编译器针对指定函数运行(“取消”)。特别是,优化编译器目前使用 try {} catch {} 块忽略函数!

因此

  • 如果您尝试了 {} catch {} 块,请将对性能敏感的代码放入嵌套函数: ```js function perf_sensitive() { // 在此处执行对性能敏感的工作 }

try { perf_sensitive() } catch (e) { // 在此处处理异常 } ```

随着我们在优化编译器中启用 try/catch 块,此指导在将来可能会发生变化。通过结合使用“--trace-opt”选项和 d8(如上所述),您可以检查优化编译器如何逐步减少函数,从而详细了解哪些函数被取消了:

d8 --trace-opt primes.js

去优化

最后,该编译器执行的优化是推测性的 - 有时它不起作用,我们会退避。“去优化”的过程会舍弃优化的代码,并在“完整”编译器代码中的正确位置恢复执行。重新优化可能会稍后再次触发,但在短期内,执行速度会减慢。特别是,如果在函数优化后,导致隐藏的变量类发生变化,则会导致这种去优化。

因此

  • 避免函数优化后隐藏类更改

与其他优化一样,您可以使用日志记录标志获取 V8 必须进行去优化的函数日志:

d8 --trace-deopt primes.js

其他 V8 工具

顺便提一下,您还可以在 Chrome 启动时将 V8 跟踪选项传递给 Chrome:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```

除了使用开发者工具进行性能分析之外,您还可以使用 d8 进行性能分析:

% out/ia32.release/d8 primes.js --prof

这会使用内置的采样分析器,它会每毫秒获取一个样本并写入 v8.log。

总结

在准备构建高性能 JavaScript 时,请务必识别和了解 V8 引擎如何与您的代码协同工作。再次强调,基本建议是:

  • 在出现问题(或发现)问题之前做好准备
  • 然后,确定并了解问题的根源
  • 最后,修正重要问题

这意味着您应该先使用 PageSpeed 等其他工具确保问题出在 JavaScript 上;在收集指标之前,可能先缩减为纯 JavaScript(无 DOM),然后使用这些指标找出瓶颈并消除重要问题。希望 Daniel 的演讲(和这篇文章)能够帮助您更好地了解 V8 是如何运行 JavaScript 的,但也请您务必将重点放在优化您自己的算法上!

参考编号