V8 中 JavaScript 的性能提示
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 代码生成单一版本的优化程序集。 您越能避免导致隐藏类发散,您获得的性能就越好。
所以
- 在构造函数中初始化所有对象成员(因此实例以后不会更改类型)
- 始终以相同的顺序初始化对象成员
数字
当类型可以更改时,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 元素)到它们的最大大小,而是随着你的增长而增长
- 不要删除数组中的元素,尤其是数字数组
- 不要加载未初始化或删除的元素:
a = new Array();
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 中,Full 编译器在所有代码上运行,并尽快开始执行代码,快速生成好的但不是很好的代码。 该编译器在编译时几乎不假设任何类型——它期望变量的类型可以并且将会在运行时改变。 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 中是个好主意的另一个原因)。
您可以使用独立的“d8”版本的 V8 引擎记录优化的内容:
d8 --trace-opt primes.js
(这会将优化函数的名称记录到标准输出。)
然而,并非所有函数都可以优化 - 某些功能会阻止优化编译器在给定函数上运行(“bail-out”)。 特别是,优化编译器目前使用 try {} catch {} 块来摆脱函数!
所以
如果您有 try {} catch {} 块,请将性能敏感代码放入嵌套函数中:
function perf_sensitive() {
// Do performance-sensitive work here
}
try {
perf_sensitive()
} catch (e) {
// Handle exceptions here
}
随着我们在优化编译器中启用 try/catch 块,该指南将来可能会发生变化。 您可以通过将“--trace-opt”选项与上述 d8 一起使用来检查优化编译器如何在函数上进行 bail out,这会为您提供有关哪些函数被 bail out 的更多信息:
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"
除了使用 developer tools profiling 之外,你还可以使用 d8 来做 profiling:
% out/ia32.release/d8 primes.js --prof
这使用内置的采样分析器,每毫秒采样一次并写入 v8.log。
总之
识别和理解 V8 引擎如何与您的代码一起工作以准备构建高性能 JavaScript 非常重要。 再一次,基本的建议是:
- 在遇到(或注意到)问题之前做好准备
- 然后,确定并理解问题的症结所在
- 最后,解决重要的问题
这意味着您应该首先使用 PageSpeed 等其他工具来确保问题出在您的 JavaScript 中; 可能在收集指标之前减少到纯 JavaScript(无 DOM),然后使用这些指标来定位瓶颈并消除重要的瓶颈。 希望 Daniel 的演讲(和这篇文章)能帮助您更好地理解 V8 如何运行 JavaScript - 但一定要专注于优化您自己的算法!
参考
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 在响应式网站上的使用矢量图形
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论