[译]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——但是也要确保专注于优化你自身的算法!