V8 性能优化杀手

本文讲的是V8 性能优化杀手,


V8 性能优化杀手

简介

这篇文章将会给你一些建议,让你避免写出性能远低于期望值的代码。在此特别指出有一些代码会导致 V8 引擎(涉及到 Node.JS、Opera、Chromium 等)无法对相关函数进行优化。

vhf 正在做一个类似的项目,试图将 V8 引擎的性能杀手全部列出来:V8 Bailout Reasons

V8 引擎背景知识

V8 引擎中没有解释器,但有 2 种不同的编译器:普通编译器与优化编译器。编译器会将你的 JavaScript 代码编译成汇编语言后直接运行。但这并不意味着运行速度会很快。被编译成汇编语言后的代码并不能显著地提高其性能,它只能省去解释器的性能开销,如果你的代码没有被优化的话速度依然会很慢。

例如,在普通编译器中 a + b 将会被编译成下面这样:

mov eax, a
mov ebx, b
call RuntimeAdd

换句话说,其实它仅仅调用了 runtime 函数。但如果 a 和 b 能确定都是整型变量,那么编译结果会是下面这样:

mov eax, a
mov ebx, b
add eax, ebx

它的执行速度会比前面那种去在 runtime 中调用复杂的 JavaScript 加法算法快得多。

通常来说,使用普通编译器将会得到前面那种代码,使用优化编译器将会得到后面那种代码。走优化编译器的代码可以说比走普通编译器的代码性能好上 100 倍。但是请注意,并不是任何类型的 JavaScript 代码都能被优化。在 JS 中,有很多种情况(甚至包括一些我们常用的语法)是不能被优化编译器优化的(这种情况被称为“bailout”,从优化编译器降级到普通编译器)。

记住一些会导致整个函数无法被优化的情况是很重要的。JS 代码被优化时,将会逐个优化函数,在优化各个函数的时候不会关心其它的代码做了什么(除非那些代码被内联在即将优化的函数中。)。

这篇文章涵盖了大多数会导致函数坠入“无法被优化的深渊”的情况。不过在未来,优化编译器进行更新后能够识别越来越多的情况时,下面给出的建议与各种变通方法可能也会变的不再必要或者需要修改。

主题

  1. 工具
  2. 不支持的语法
  3. 使用 arguments
  4. Switch-case
  5. For-in
  6. 退出条件藏的很深,或者没有定义明确出口的无限循环

1. 工具

你可以在 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

事实证明,使用这个工具来验证处理方法是可行且必要的。

2. 不支持的语法

有一些语法结构是不支持被编译器优化的,用这类语法将会导致包含在其中的函数不能被优化。

请注意,即使这些语句不会被访问到或者不会被执行,它仍然会导致整个函数不能被优化。

例如下面这样做是没用的:

if (DEVELOPMENT) {
    debugger;
}

即使 debugger 语句根本不会被执行到,上面的代码将会导致包含它的整个函数都不能被优化。

目前不可被优化的语法有:

  • Generator 函数 (V8 5.7 对其做了优化)
  • 包含 for of 语句的函数 (V8 commit 11e1e20 对其做了优化)
  • 包含 try catch 语句的函数 (V8 commit 9aac80f / V8 5.3 / node 7.x 对其做了优化)
  • 包含 try finally 语句的函数 (V8 commit 9aac80f / V8 5.3 / node 7.x 对其做了优化)
  • 包含let 复合赋值的函数 (Chrome 56 / V8 5.6! 对其做了优化)
  • 包含 const 复合赋值的函数 (Chrome 56 / V8 5.6! 对其做了优化)
  • 包含 __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 是返回值
}

3. 使用 arguments

有许多种使用 arguments 的方式会导致函数不能被优化。因此当使用 arguments 的时候需要格外小心。

3.1. 在非严格模式中,对一个已经被定义,同时在函数体中被 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')。

3.2. arguments 泄露:

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;
}

3.3. 对 arguments 进行赋值:

在非严格模式下可以这么做:

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 对象的分配。

4. Switch-case

在以前,一个 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

5. For-in

For-in 语句在某些情况下会导致整个函数无法被优化。

这也解释了”For-in 速度不快“之类的说法。

5.1. 键不是局部变量:

function nonLocalKey1() {
    var obj = {}
    for(var key in obj);
    return function() {
        return key;
    };
}
var key;
function nonLocalKey2() {
    var obj = {}
    for(key in obj);
}

这两种用法db都将会导致函数不能被优化的问题。因此键不能在上级作用域定义,也不能在下级作用域被引用。它必须是一个局部变量。

5.2. 被遍历的对象不是一个”简单可枚举对象“

5.2.1. 处于”哈希表模式“(又被称为”归一化对象“或”字典模式对象“ - 这种对象将哈希表作为其数据结构)的对象不是简单可枚举对象。
function hashTableIteration() {
    var hashTable = {"-": 3};
    for(var key in hashTable);
}

如果你给一个对象动态增加了很多的属性(在构造函数外)、delete 属性或者使用不合法的标识符作为属性,这个对象将会变成哈希表模式。换句话说,当你把一个对象当做哈希表来用,它就真的会变成哈希表。请不要对这种对象使用 for-in。你可以用过开启 Node.JS 的--allow-natives-syntax,调用 console.log(%HasFastProperties(obj)) 来判断一个对象是否为哈希表模式。


5.2.2. 对象的原型链中存在可枚举属性
Object.prototype.fn = function() {};

上面这么做会给所有对象(除了用 Object.create(null) 创建的对象)的原型链中添加一个可枚举属性。此时任何包含了 for-in 语法的函数都不会被优化(除非仅遍历Object.create(null) 创建的对象)。

你可以使用 Object.defineProperty 创建不可枚举属性(不推荐在 runtime 中调用,但是在定义一些例如原型属性之类的静态数据的时候它很高效)。


5.2.3. 对象中包含可枚举数组索引

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;
}

6. 退出条件藏的很深,或者没有定义明确出口的无限循环

有时候在你写代码的时候,你需要用到循环,但是不确定循环体内的代码之后会是什么样子。所以这时候你用了一个 while (true) { 或者 for (;;) {,在之后将终止条件放在循环体中,打断循环进行后面的代码。然而你写完这些之后就忘了这回事。在重构时,你发现这个函数很慢,出现了反优化情况 - 上面的循环很可能就是罪魁祸首。

重构时将循环内的退出条件放到循环的条件部分并不是那么简单。

  1. 如果代码中的退出条件是循环最后的 if 语句的一部分,且代码至少要运行一轮,那么你可以将这个循环重构为 do{} while ();
  2. 如果退出条件在循环的开头,请将它放在循环的条件部分中去。
  3. 如果退出条件在循环体中部,你可以尝试”滚动“代码:试着依次将一部分退出条件前的代码移到后面去,然后在之前的位置留下它的引用。当退出条件可以放在循环条件部分,或者至少变成一个浅显的逻辑判断时,这个循环就不再会出现反优化的情况了。





原文发布时间为:2017年7月3日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-02 01:25:26

V8 性能优化杀手的相关文章

JRuby的性能优化(update)

越来越觉的JRuby是个很有前途的项目,结合Ruby的性感语法和java极其丰富的类库,况且有团队持续不断地修正bug.改进性能,这样的玩意完全有成为"少男杀手"的潜质.JRuby wiki上列出了性能优化的四条建议: 1.调优编译器,JRuby早就弃暗投明跟随XRuby走上了编译这条牛B的道路,将Ruby Script编译成字节码,因此这个环节是断断不能忽略的. 两种编译方式: AOT模式:直接生成class文件,脱了Ruby这层皮,咱就是人见人"爱"的java了

nodejs的10个性能优化技巧_node.js

下面是我们使用Node.js时遵循的10个性能规则: 1. 避免使用同步代码 在设计上,Node.js是单线程的.为了能让一个单线程处理许多并发的请求,你可以永远不要让线程等待阻塞,同步或长时间运行的操作.Node.js的一个显著特征是:它从上到下的设计和实现都是为了实现异步.这让它非常适合用于事件型程序. 不幸的是,还是有可能会发生同步/阻塞的调用.例如,许多文件系统操作同时拥有同步和异步的版本,比如writeFile和writeFileSync.即使你用代码来控制同步方法,但还是有可能不注意

SQL Server 查询性能优化 相关文章

来自: SQL Server 查询性能优化--堆表.碎片与索引(一) SQL Server 查询性能优化--堆表.碎片与索引(二) SQL Server 查询性能优化--覆盖索引(一) SQL Server 查询性能优化--覆盖索引(二) SQL Server 查询性能优化--创建索引原则(一) SQL Server 查询性能优化--创建索引原则(二) SQL Server 查询性能优化--索引与SARG(一) SQL Server 查询性能优化--索引与SARG(二) SQL Server 查

丰趣海淘:跨境电商平台的前端性能优化实践

原文出自[听云技术博客]:http://blog.tingyun.com/web/article/detail/586 随着互联网的发展,尤其是在2000年之后浏览器技术渐渐成熟,Web产品也越来越丰富,这时我们被浏览器窗口内的丰富"内容"所吸引,关注HTML/CSS,深入研究Dom.Bom和浏览器的渲染机制等,接触JavaScript库,"前端"这个职业,由此而生. 前端技术在这10多年中飞速发展,到了今天,我们可能发现"内容"的美在视觉上是有

如何确保无线局域网性能优化应

虽然很多无线局域网经理关注于解决问题和避免安全漏洞,但是无线局域网性能优化更注重于任务清单.这个方法不单单考虑不周全,而且忽视优化还是造成灾难的另一个因素.如果每个AP只需要支持十来个用户上网流量,一般的性能可能还是可以接受的,但是当AP接近最大容量而应用程序需求变得更高时,带宽消耗和瓶颈就会变得很严重. 为了实现无线局域网性能优化,网络经理和管理员必须使用新的无线局域网测量和测试工具,它们能够根据网络中运行的应用程序类型来检查网络性能.寻找这些工具会迫使网络管理员寻找目前的供应商以及测试方法之

性能优化总结(六):预加载、聚合SQL应用实例

   前面已经把原理都讲了一遍,这篇主要是给出一个应用的实例.该实例取自GIX4,比较复杂. 领域模型:     领域模型间的关系,如下: 右边模型链的具体关系在<第二篇>中已经描述过,不再赘述. 本次重点在于红线框住部分: Project:表示一个建设项目: ProjectPBS:一个项目下包含的很多PBS: PBSPropertyValue:一个PBS我们可以为它设置多个值,每一个值对应一个PBSType(模板)中已定义的属性,值的范围也是只能在属性中已定义的可选值中进行选择. 对应的UI

性能优化-位运算效率高还是位运算效率高?

问题描述 位运算效率高还是位运算效率高? 在<java程序性能优化>中写到,位运算是运算中最高效的,而我依照书上的代码运行了一遍 发现并不是这样. 补上代码: public class BitOperate { public static void main(String[] args) { nomal(); bit(); } private static void nomal() { long start =System.currentTimeMillis(); long a = 100;

面向程序员的数据库访问性能优化法则

面向程序员的数据库访问性能优化法则     http://blog.csdn.net/yzsind/archive/2010/12/06/6059209.aspx

jQuery性能优化指南

jQuery性能优化指南,可以从以下12个方向考虑. 1,总是从ID选择器开始继承 2,在class前使用tag(标签名) 3,将jQuery对象缓存起来(在多次使用是,用一个中间变量代替,而不是总是用选择器)   4,对直接的DOM操作进行限制 5,注意尽量减少事件冒泡 6,推迟到 $(window).load   7,压缩JavaScript 8,尽量使用ID代替Class. 9,给选择器一个上下文   10,慎用 .live()方法(应该说尽量不要使用) 11,子选择器和后代选择器   1