V8 中的 JavaScript 性能提示

Chris Wilson
Chris Wilson

简介

Daniel Clifford 在 Google I/O 大会上发表了精彩的演讲,介绍了提升 V8 中的 JavaScript 性能的提示和技巧。小尼鼓励我们“提高需求量”- 仔细分析 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] 的赋值会导致该数组转换为未装箱的双精度数组,但 a[3] 的赋值会导致它重新转换回可包含任何值(数字或对象)的数组。在第二种情况下,编译器知道字面量中所有元素的类型,并且可以预先确定隐藏类。

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

JavaScript 编译

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

  • “完整”它可为任何 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”来记录以下版本:

d8 --trace-opt primes.js

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

不过,并非所有函数都可以优化,某些功能会阻止优化编译器在给定函数上运行(“退出”)。特别要指出的是,优化编译器目前会释放包含 try {}catch {} 块的函数!

因此

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

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

随着我们在优化编译器中启用 try/catch 块,本指南将来可能会发生变化。您可以使用“--trace-opt”命令,检查优化编译器如何抛弃函数,与上述 d8 搭配使用,您可以详细了解被淘汰的函数:

d8 --trace-opt primes.js

去优化

最后,此编译器执行的优化是推测性的 - 有时行不通,就会退避。“去优化”的过程舍弃经过优化的代码,并在“full”的适当位置继续执行,编译代码。重新优化操作稍后可能会再次触发,但短期内,执行速度会变慢。特别是,如果函数优化后隐藏的变量类发生变化,则会导致去优化。

因此

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

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

d8 --trace-deopt primes.js

其他 V8 工具

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

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

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

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

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

总结

请务必确定并了解 V8 引擎如何与代码协同工作,以便为构建高性能 JavaScript 做好准备。再次强调,我们的基本建议是:

  • 做好准备,在遇到问题(或发现问题)之前
  • 然后,确定和了解问题的关键所在
  • 最后,解决重要问题

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

参考