v8中javascript的性能提示
将任何性能建议放在上下文中很重要。性能优化是上瘾的,有时专注于深层次的咨询首先可以从真正的问题中分心。您需要全面了解您的Web应用程序的性能 - 在关注这些性能提示之前,您应该可以使用PageSpeed之类的工具来分析您的代码,并获得分数。这将帮助您避免过早优化。
在Web应用程序中获得良好性能的最佳基本建议是:
•在您(或通知)问题之前做好准备
•然后,确定并了解问题的症结所在
•最后,处理重要问题
为了完成这些步骤,了解V8如何优化JS可能很重要,因此您可以编写符合JS运行时设计的代码。了解可用的工具以及如何帮助您也很重要。丹尼尔谈谈如何使用开发者工具,该文件只是捕捉了V8引擎设计中的一些最重要的一点。
Hidden Classes
JavaScript有有限的编译时类型信息:类型可以在运行时更改,所以很自然地期望在编译时对JS类型进行推理代价是昂贵的。 这可能会导致您质疑JavaScript性能如何在任何接近C ++的地方。 然而,V8具有在运行时内部为对象创建的隐藏类型; 具有相同隐藏类的对象可以使用相同的优化生成代码。
For example:
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从使用您处理的数字类型的值推断出类型。一旦v8完成了这一推断,它使用标记来有效地表示值,因为这些类型可以动态变化。然而,更改这些类型标记有时需要额外花费,所以最好使用数字类型来保持一致,一般来说,在适当的时候使用31位有符号整数是最优的。
For example:
var i = 42; // this is a 31-bit signed integer var j = 4.2; // this is a double-precision floating point number
因此:
·更偏好可以表示为31位有符号整数的数值。
数组
为了处理大型和稀疏数组,内部有两种类型的数组存储:
·快速元素:紧凑键集的线性存储
·否则用 dictionary元素:哈希表存储
最好不要将数组存储从一个类型转换为另一个类型。
因此:
·为数组使用从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. }
此外,偶数数组更快-数组隐藏的类会追踪元素类型,并且偶数个元素的数组是unboxed(它导致隐藏的类更改)。然而,因为boxing和unboxing机制,对数组的粗心操作会导致额外的工作,如:
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];
因为在第一个例子中,个别的分配是一个接一个执行的,而一个[2]的赋值使得Array被转换成一个未装箱的双精度数组,但是一个[3]的赋值使它被重新生成, 转换回可以包含任何值(数字或对象)的数组。 在第二种情况下,编译器知道文字中所有元素的类型,隐藏类可以在前面确定。
因此:
•使用数组文字初始化小型固定大小的数组
•在使用小数组(<64k)之前对其进行预分配以校正大小
•不要在数字数组中存储非数值(对象)
•如果您没有文字初始化,请小心不要引起小数组的重新转换。
JavaScript编译
虽然JavaScript是一种非常动态的语言,并且它的原始实现是解释器,但现代JavaScript运行时引擎使用编译。 V8(Chrome的JavaScript)有两个不同的即时(JIT)编译器,实际上是:
•“完整”编译器,可以为任何JavaScript生成好的代码
•优化编译器,为大多数JavaScript生成出色的代码,但编译时间较长。
全编译器
在V8中,完全编译器运行在所有代码上,并尽快开始执行代码,快速生成好但不是很好的代码。这个编译器在编译时几乎不关心类型 - 它希望类型的变量可以在运行时改变。完整编译器生成的代码使用内联缓存(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 {}块的功能!
因此:
•如果您尝试{} catch {}块,则将敏感代码放入嵌套函数中:
function perf_sensitive() { // Do performance-sensitive work here } try { perf_sensitive() } catch (e) { // Handle exceptions here }
由于我们在优化编译器中启用了try / catch块,所以这个指导将来可能会改变。 您可以通过使用上述d8的“–trace-opt”选项来检查优化编译器是如何缓解功能的,这样您可以获得有关哪些函数被保释的更多信息:
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。 再一次,基本建议是:•在您(或通知)问题之前做好准备
•然后,确定并了解问题的症结所在
•最后,确定重要
这意味着您应该确保您的JavaScript中的问题,首先使用其他工具,如PageSpeed; 在收集指标之前,可能会降低到纯JavaScript(无DOM),然后使用这些指标来定位瓶颈并消除重要的指标。 希望Daniel的话(和本文)将帮助您更好地了解V8如何运行JavaScript - 但一定要专注于优化您自己的算法!