简介
Daniel Clifford 在 Google I/O 大会上发表了精彩演讲,分享了在 V8 中提升 JavaScript 性能的提示和技巧。Daniel 鼓励我们“追求更快的速度”,即仔细分析 C++ 和 JavaScript 之间的性能差异,并在编写代码时充分考虑 JavaScript 的工作原理。本文总结了 Daniel 演讲中最重要的要点,并且我们也会在性能指南发生变化时更新本文。
最重要的建议
请务必结合具体情况来考虑任何性能建议。性能建议令人着迷,但有时过早关注深层建议可能会分散注意力,导致无法发现真正的问题。您需要全面了解 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 位有符号整数的数值。
数组
为了处理大型稀疏数组,内部有两种类型的数组存储:
- Fast Elements:用于紧凑键集的线性存储
- 字典元素:否则为哈希表存储
最好不要导致数组存储从一种类型切换到另一种类型。
因此
- 对数组使用从 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] 的赋值会导致数组转换为未装箱 double 值的数组,但随后对 a[3] 的赋值会导致其重新转换回可包含任何值(数字或对象)的数组。在第二种情况下,编译器知道字面量中所有元素的类型,并且可以预先确定隐藏类。
- 对于小型固定大小数组,使用数组字面量进行初始化
- 在使用小型数组 (<64k) 之前,请先将其预分配到正确的大小
- 不要在数值数组中存储非数值(对象)
- 如果您在初始化时不使用字面量,请注意不要导致小型数组重新转换。
JavaScript 编译
虽然 JavaScript 是一种非常动态的语言,并且其原始实现是解释器,但现代 JavaScript 运行时引擎使用编译。事实上,V8(Chrome 的 JavaScript 引擎)有两个不同的即时 (JIT) 编译器:
- “完整”编译器,可为任何 JavaScript 生成优质代码
- 优化编译器,可为大多数 JavaScript 生成出色的代码,但编译时间较长。
完整编译器
在 V8 中,完整编译器会在所有代码上运行,并尽快开始执行代码,从而快速生成良好但并非出色的代码。此编译器在编译时间几乎不假设类型,它预计变量的类型可以在运行时更改,并且会更改。Full 编译器生成的代码使用内联缓存 (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 中是一个好主意的另一个原因)。
您可以使用 V8 引擎的独立“d8”版本记录优化的内容:
d8 --trace-opt primes.js
(这会将优化函数的名称记录到 stdout。)
不过,并非所有函数都可以优化,某些功能会阻止优化编译器在给定函数上运行(“退出”)。特别是,优化编译器目前会跳过包含 try {} catch {} 块的函数!
因此
- 如果您有 try {} 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 工具
顺便提一下,您还可以在启动时将 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。
总结
请务必了解 V8 引擎如何与您的代码搭配使用,以便为构建高性能 JavaScript 做好准备。同样,基本建议如下:
- 在遇到(或发现)问题之前做好准备
- 然后,找出并了解问题的症结所在
- 最后,修复重要问题
这意味着,您应首先使用 PageSpeed 等其他工具,确保问题出在 JavaScript 中;在收集指标之前,可能需要将代码简化为纯 JavaScript(无 DOM),然后使用这些指标找出瓶颈并消除重要瓶颈。希望 Daniel 的演讲(以及本文)能帮助您更好地了解 V8 如何运行 JavaScript,但请务必专注于优化您自己的算法!