V8 中 JavaScript 的性能提示

发布于 2023-05-13 22:19:19 字数 6005 浏览 58 评论 0

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

英雄似剑

暂无简介

0 文章
0 评论
675 人气
更多

推荐作者

yili302

文章 0 评论 0

晚霞

文章 0 评论 0

LLFFCC

文章 0 评论 0

陌路黄昏

文章 0 评论 0

xiaohuihui

文章 0 评论 0

你与昨日

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文