1

[译]JavaScript V8性能小贴士

 8 months ago
source link: https://jiongks.name/blog/v8-javascript-performance-tips
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[译]JavaScript V8性能小贴士

译自:Performance Tips for JavaScript in V8

简介

关于如何巧妙提高V8 JavaScript性能的话题,Daniel Clifford在Google I/O上做了一次非常精彩的分享。Daniel鼓励我们“追求更快”,认真的分析C++和JavaScript之间的性能差距,根据JavaScript的工作原理撰写代码。在Daniel的分享中,有一个核心要点的归纳,我们也会根据性能指导的变化保持对这篇文章的更新。

最重要的建议

最重要的是要把任何性能建议放在特定的情境当中。性能建议是附加的东西,有时一开始就特别注意深层的建议反而会对我们造成干扰。你需要从一个综合的角度看待你的Web应用的性能——在关注这些性能建议之前,你应该找PageSpeed之类的工具大概分析一下你的代码,也算是跑个分先。这会防止你过度优化。

对Web应用的性能优化,几个原则性的建议是:

  • 首先,未雨绸缪
  • 然后,找到症结
  • 最后,修复它

为了完成这几个步骤,理解V8如何优化JS是一件很重要的事情,这样你就可以根据其对JS运行时的设计撰写代码。同样重要的是掌握一些帮得上忙的工具。Daniel也交代了一些开发者工具的用法,它们刚好抓住了一些V8引擎设计上最重要的部分。

OK。开始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
// 这里的p1和p2拥有共享的隐藏类
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!
// 注意!这时p1和p2的隐藏类已经不同了!

在我们为p2添加“z”这个成员之前,p1和p2一直共享相同的内部隐藏类——所以V8可以生成一段单独版本的优化汇编码,这段代码可以同时封装p1和p2的JavaScript代码。我们越避免隐藏类的派生,就会获得越高的性能。

结论

  • 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类)
  • 总是以相同的次序初始化对象成员

数字

当类型可以改变时,V8使用标记来高效的标识其值。V8通过其值来推断你会以什么类型的数字来对待它。因为这些类型可以动态改变,所以一旦V8完成了推断,就会通过标记高效完成值的标识。不过有的时候改变类型标记还是比较消耗性能的,我们最好保持数字的类型始终不变。通常标识为有符号的31位整数是最优的。

var i = 42; // 这是一个31位有符号整数
var j = 4.2; // 这是一个双精度浮点数

结论

  • 尽量使用可以用31位有符号整数表示的数。

数组

为了掌控大而稀疏的数组,V8内部有两种数组存储方式:

  • 快速元素:对于紧凑型关键字集合,进行线性存储
  • 字典元素:对于其它情况,使用哈希表

最好别导致数组存储方式在两者之间切换。

结论

  • 使用从0开始连续的数组关键字
  • 别预分配大数组(比如大于64K个元素)到其最大尺寸,令其尺寸顺其自然发展就好
  • 别删除数组里的元素,尤其是数字数组
  • 别加载未初始化或已删除的元素:
a = new Array();
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // 杯具!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
  a[0] |= b;  // 比上面快2倍
}

同样的,双精度数组会更快——数组的隐藏类会根据元素类型而定,而只包含双精度的数组会被拆箱(unbox),这导致隐藏类的变化。对数组不经意的封装就可能因为装箱/拆箱(boxing/unboxing)而导致额外的开销。比如:

var a = new Array();
a[0] = 77; // 分配
a[1] = 88;
a[2] = 0.5; // 分配,转换
a[3] = true; // 分配,转换

下面的写法效率更高:

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

因为第一个例子是一个一个分配赋值的,并且对a[2]的赋值导致数组被拆箱为了双精度。但是对a[3]的赋值又将数组重新装箱回了任意值(数字或对象)。第二种写法时,编译器一次性知道了所有元素的字面上的类型,隐藏隐藏类可以直接确定。

结论

  • 初始化小额定长数组时,用字面量进行初始化
  • 小数组(小于64k)在使用之前先预分配正确的尺寸
  • 请勿在数字数组中存放非数字的值(对象)
  • 如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换

JavaScript编译

尽管JavaScript是个非常动态的语言,且原本的实现是解释性的,但现代的JavaScript运行时引擎都会进行编译。V8(Chrome的JavaScript)有两个不同的运行时(JIT)编译器:

  • “完全”编译器,可以为任何JavaScript生成优秀的代码
  • 优化编译器,可以为大部分JavaScript生成伟大(汗一下自己的翻译)的代码,但会更耗时

完全编译器

在V8中,完全编译器会以最快的速度运行在任何代码上,快速生成优秀但不伟大的代码。该编译器在编译时几乎不做任何有关类型的假设——它预测类型在运行时会发生改变。完全编译器的生成码通过内联缓存(ICs)在程序运行时提炼类型相关的知识,以便将来改进和优化。

内联缓存的目的是,通过缓存依赖类型的代码进行操作,更有效率的掌控类型。当代吗运行时,它会先验证对类型的假设,然后使用内联缓存快速执行操作。这也意味着可以接受多种类型的操作会变得效率低下。

结论

  • 单态操作优于多态操作

如果一个操作的输入总是相同类型的,则其为单态操作。否则,操作调用时的某个参数可以跨越不同的类型,那就是多态操作。比如add()的第二个调用就触发了多态操作:

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

add(1, 2);     // add中的+操作是单态操作
add("a", "b"); // add中的+操作变成了多态操作

优化编译器

V8有一个和完全编译器并行的优化编译器,它会重编那些最“热门”(即被调用多次)的函数。优化编译器通过类型反馈来使得编译过的代码更快——事实上它就是使用了我们之前谈到的ICs的类型信息!

在优化编译器里,操作都是内联的(直接出现在被调用的地方)。它加速了执行(拿内存空间换来的),同时也进行了各种优化。单态操作的函数和构造函数可以整个内联起来(这是V8中单态操作的有一个好处)。

你可以使用单独的“d8”版本的V8引擎来获取优化记录:

d8 --trace-opt primes.js

(其会把被优化的函数名输出出来)

不是所有的函数都可以被优化,有些特性会阻止优化编译器运行一个已知函数(bail-out)。目前优化编译器会排除有try/catch的代码块的函数。

结论

  • 如果存在try/catch代码快,则将性能敏感的代码放到一个嵌套的函数中:
function perf_sentitive() {
 // 把性能敏感的工作放置于此
}

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

这个建议可能会在未来发生改变,因为我们会在优化编译器里开启try/catch代码块。你可以通过使用上述的d8选项“--trace-opt”得到更多有关这些函数的信息来检验优化编译器如何排除这些函数。

d8 --trace-opt primes.js

取消优化

最终,编译器的性能优化是有针对性的——有时它的变现并不好,我们就不得不回退。“取消优化”的过程实际上就是把优化过的代码扔掉,恢复执行完全编译器的代码。重优化可能稍后再打开,但是短期内性能会下降。尤其是取消优化的发生会导致其函数的变量的隐藏类的变化。

结论

  • 回避在优化过后函数内隐藏类改变

你可以像其它优化一样,通过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——但是也要确保专注于优化你自身的算法!

参考资料


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK