- 原文地址:Optimization killers
- 原文做者:github.com/petkaantono…
- 译文出自:掘金翻译计划
- 译者:lsvih
- 校对者:Aladdin-ADD,zhaochuanxing
这篇文章将会给你一些建议,让你避免写出性能远低于指望值的代码。在此特别指出有一些代码会致使 V8 引擎(涉及到 Node.JS、Opera、Chromium 等)没法对相关函数进行优化。html
vhf 正在作一个相似的项目,试图将 V8 引擎的性能杀手所有列出来:V8 Bailout Reasons。前端
V8 引擎中没有解释器,但有 2 种不一样的编译器:普通编译器与优化编译器。编译器会将你的 JavaScript 代码编译成汇编语言后直接运行。但这并不意味着运行速度会很快。被编译成汇编语言后的代码并不能显著地提升其性能,它只能省去解释器的性能开销,若是你的代码没有被优化的话速度依然会很慢。node
例如,在普通编译器中 a + b
将会被编译成下面这样:react
mov eax, a
mov ebx, b
call RuntimeAdd复制代码
换句话说,其实它仅仅调用了 runtime 函数。但若是 a
和 b
能肯定都是整型变量,那么编译结果会是下面这样:android
mov eax, a
mov ebx, b
add eax, ebx复制代码
它的执行速度会比前面那种去在 runtime 中调用复杂的 JavaScript 加法算法快得多。ios
一般来讲,使用普通编译器将会获得前面那种代码,使用优化编译器将会获得后面那种代码。走优化编译器的代码能够说比走普通编译器的代码性能好上 100 倍。可是请注意,并非任何类型的 JavaScript 代码都能被优化。在 JS 中,有不少种状况(甚至包括一些咱们经常使用的语法)是不能被优化编译器优化的(这种状况被称为“bailout”,从优化编译器降级到普通编译器)。git
记住一些会致使整个函数没法被优化的状况是很重要的。JS 代码被优化时,将会逐个优化函数,在优化各个函数的时候不会关心其它的代码作了什么(除非那些代码被内联在即将优化的函数中。)。github
这篇文章涵盖了大多数会致使函数坠入“没法被优化的深渊”的状况。不过在将来,优化编译器进行更新后可以识别愈来愈多的状况时,下面给出的建议与各类变通方法可能也会变的再也不必要或者须要修改。算法
你能够在 node.js 中使用一些 V8 自带的标记来验证不一样的代码用法对优化的影响。一般来讲你能够建立一个包括特定模式的函数,而后使用全部容许的参数类型去调用它,再使用 V8 的内部去优化与检查它:后端
test.js:
//建立包含须要检查的状况的函数(检查使用 `eval` 语句是否能被优化)
function exampleFunction() {
return 3;
eval('');
}
function printStatus(fn) {
switch(%GetOptimizationStatus(fn)) {
case 1: console.log("Function is optimized"); break;
case 2: console.log("Function is not optimized"); break;
case 3: console.log("Function is always optimized"); break;
case 4: console.log("Function is never optimized"); break;
case 6: console.log("Function is maybe deoptimized"); break;
case 7: console.log("Function is optimized by TurboFan"); break;
default: console.log("Unknown optimization status"); break;
}
}
//识别类型信息
exampleFunction();
//这里调用 2 次是为了让这个函数状态从 uninitialized -> pre-monomorphic -> monomorphic
exampleFunction();
%OptimizeFunctionOnNextCall(exampleFunction);
//再次调用
exampleFunction();
//检查
printStatus(exampleFunction);复制代码
运行它:
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
(v0.12.7) Function is not optimized
(v4.0.0) Function is optimized by TurboFan复制代码
codereview.chromium.org/1962103003
为了检验咱们作的这个工具是否真的有用,注释掉 eval
语句而后再运行一次:
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
[optimizing 000003FFCBF74231 <JS Function exampleFunction (SharedFunctionInfo 00000000FE1389E1)> - took 0.345, 0.042, 0.010 ms]
Function is optimized复制代码
事实证实,使用这个工具来验证处理方法是可行且必要的。
有一些语法结构是不支持被编译器优化的,用这类语法将会致使包含在其中的函数不能被优化。
请注意,即便这些语句不会被访问到或者不会被执行,它仍然会致使整个函数不能被优化。
例以下面这样作是没用的:
if (DEVELOPMENT) {
debugger;
}复制代码
即便 debugger 语句根本不会被执行到,上面的代码将会致使包含它的整个函数都不能被优化。
目前不可被优化的语法有:
let
复合赋值的函数const
复合赋值的函数__proto__
对象字面量、get
声明、set
声明的函数看起来永远不会被优化的语法有:
debugger
语句的函数eval()
的函数with
语句的函数最后明确一下:若是你用了下面任何一种状况,整个函数将不能被优化:
function containsObjectLiteralWithProto() {
return {__proto__: 3};
}复制代码
function containsObjectLiteralWithGetter() {
return {
get prop() {
return 3;
}
};
}复制代码
function containsObjectLiteralWithSetter() {
return {
set prop(val) {
this.val = val;
}
};
}复制代码
另外在此要特别提一下 eval
和 with
,它们会致使它们的调用栈链变成动态做用域,可能会致使其它的函数也受到影响,由于这种状况没法从字面上判断各个变量的有效范围。
变通办法
前面提到的不能被优化的语句用在生产环境代码中是没法避免的,例如 try-finally
和 try-catch
。为了让使用这些语句的影响尽可能减少,它们须要被隔离在一个最小化的函数中,这样主要的函数就不会被影响:
var errorObject = {value: null};
function tryCatch(fn, ctx, args) {
try {
return fn.apply(ctx, args);
}
catch(e) {
errorObject.value = e;
return errorObject;
}
}
var result = tryCatch(mightThrow, void 0, [1,2,3]);
//明确地报出 try-catch 会抛出什么
if(result === errorObject) {
var error = errorObject.value;
}
else {
//result 是返回值
}复制代码
arguments
有许多种使用 arguments
的方式会致使函数不能被优化。所以当使用 arguments
的时候须要格外当心。
arguments
引用的参数从新赋值。典型案例:function defaultArgsReassign(a, b) {
if (arguments.length < 2) b = 5;
}复制代码
变通方法 是将参数值保存在一个新的变量中:
function reAssignParam(a, b_) {
var b = b_;
//与 b_ 不一样,能够安全地对 b 进行从新赋值
if (arguments.length < 2) b = 5;
}复制代码
若是仅仅是像上面这样用 arguments
(上面代码做用为检测第二个参数是否存在,若是不存在则赋值为 5),也能够用 undefined
检测来代替这段代码:
function reAssignParam(a, b) {
if (b === void 0) b = 5;
}复制代码
可是以后若是须要用到 arguments
,很容易忘记须要在这儿加上从新赋值的语句。
变通方法 2:为整个文件或者整个函数开启严格模式 ('use strict'
)。
function leaksArguments1() {
return arguments;
}复制代码
function leaksArguments2() {
var args = [].slice.call(arguments);
}复制代码
function leaksArguments3() {
var a = arguments;
return function() {
return a;
};
}复制代码
arguments
对象在任何地方都不容许被传递或者被泄露。
变通方法 能够经过建立一个数组来代理 arguments
对象:
function doesntLeakArguments() {
//.length 仅仅是一个整数,不存在泄露
//arguments 对象自己的问题
var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
//i 是 arguments 对象的合法索引值
args[i] = arguments[i];
}
return args;
}
function anotherNotLeakingExample() {
var i = arguments.length;
var args = [];
while (i--) args[i] = arguments[i];
return args
}复制代码
可是这样要写不少让人烦的代码,所以得判断是否真的值得这么作。后面一次又一次的优化会代理更多的代码,愈来愈多的代码意味着代码自己的意义会被逐渐淹没。
不过,若是你有 build 这个过程,能够将上面这一系列过程由一个不须要 source map 的宏来实现,保证代码为合法的 JavaScript:
function doesntLeakArguments() {
INLINE_SLICE(args, arguments);
return args;
}复制代码
Bluebird 就使用了这个技术,上面的代码通过 build 以后会被拓展成下面这样:
function doesntLeakArguments() {
var $_len = arguments.length;
var args = new Array($_len);
for(var $_i = 0; $_i < $_len; ++$_i) {
args[$_i] = arguments[$_i];
}
return args;
}复制代码
在非严格模式下能够这么作:
function assignToArguments() {
arguments = 3;
return arguments;
}复制代码
变通方法:犯不着写这么蠢的代码。另外,在严格模式下它会报错。
arguments
呢?只使用:
arguments.length
arguments[i]
i
须要始终为 arguments 的合法整型索引,且不容许越界.length
和 [i]
,不要直接使用 arguments
fn.apply(y, arguments)
是没问题的,但除此以外都不行(例如 .slice
)。 Function#apply
是特别的存在。fn.$inject = ...
)和绑定函数(即 Function#bind
的结果)会生成隐藏类,所以此时使用 #apply
不安全。若是你按照上面的安全方式作,毋需担忧使用 arguments
致使不肯定 arguments 对象的分配。
在之前,一个 switch-case 语句最多只能包含 128 个 case 代码块,超过这个限制的 switch-case 语句以及包含这种语句的函数将不能被优化。
function over128Cases(c) {
switch(c) {
case 1: break;
case 2: break;
case 3: break;
...
case 128: break;
case 129: break;
}
}复制代码
你须要让 case 代码块的数量保持在 128 个以内,不然应使用函数数组或者 if-else。
这个限制如今已经被解除了,请参阅此 comment。
For-in 语句在某些状况下会致使整个函数没法被优化。
这也解释了”For-in 速度不快“之类的说法。
function nonLocalKey1() {
var obj = {}
for(var key in obj);
return function() {
return key;
};
}复制代码
var key;
function nonLocalKey2() {
var obj = {}
for(key in obj);
}复制代码
这两种用法db都将会致使函数不能被优化的问题。所以键不能在上级做用域定义,也不能在下级做用域被引用。它必须是一个局部变量。
function hashTableIteration() {
var hashTable = {"-": 3};
for(var key in hashTable);
}复制代码
若是你给一个对象动态增长了不少的属性(在构造函数外)、delete
属性或者使用不合法的标识符做为属性,这个对象将会变成哈希表模式。换句话说,当你把一个对象当作哈希表来用,它就真的会变成哈希表。请不要对这种对象使用 for-in
。你能够用过开启 Node.JS 的 --allow-natives-syntax
,调用 console.log(%HasFastProperties(obj))
来判断一个对象是否为哈希表模式。
Object.prototype.fn = function() {};复制代码
上面这么作会给全部对象(除了用 Object.create(null)
建立的对象)的原型链中添加一个可枚举属性。此时任何包含了 for-in
语法的函数都不会被优化(除非仅遍历 Object.create(null)
建立的对象)。
你可使用 Object.defineProperty
建立不可枚举属性(不推荐在 runtime 中调用,可是在定义一些例如原型属性之类的静态数据的时候它很高效)。
ECMAScript 262 规范 定义了一个属性是否有数组索引:
数组对象会给予一些种类的属性名特殊待遇。对一个属性名 P(字符串形式),当且仅当 ToString(ToUint32(P)) 等于 P 而且 ToUint32(P) 不等于 232−1 时,它是个 数组索引 。一个属性名是数组索引的属性也叫作元素 。
通常只有数组有数组索引,可是有时候通常的对象也可能拥有数组索引: normalObj[0] = value;
function iteratesOverArray() {
var arr = [1, 2, 3];
for (var index in arr) {
}
}复制代码
所以使用 for-in
进行数组遍历不只会比 for 循环要慢,还会致使整个包含 for-in
语句的函数不能被优化。
若是你试图使用 for-in
遍历一个非简单可枚举对象,它会致使包含它的整个函数不能被优化。
变通方法:只对 Object.keys
使用 for-in
,若是要遍历数组需使用 for 循环。若是非要遍历整个原型链上的属性,须要将 for-in
隔离在一个辅助函数中以下降影响:
function inheritedKeys(obj) {
var ret = [];
for(var key in obj) {
ret.push(key);
}
return ret;
}复制代码
有时候在你写代码的时候,你须要用到循环,可是不肯定循环体内的代码以后会是什么样子。因此这时候你用了一个 while (true) {
或者 for (;;) {
,在以后将终止条件放在循环体中,打断循环进行后面的代码。然而你写完这些以后就忘了这回事。在重构时,你发现这个函数很慢,出现了反优化状况 - 上面的循环极可能就是罪魁祸首。
重构时将循环内的退出条件放到循环的条件部分并非那么简单。
do{} while ();
。掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。