本文来源于知乎上的一个提问。javascript
为了程序的易读性,咱们会使用 ES6 的解构赋值:java
function f({a,b}){}
f({a:1,b:2});
复制代码
这个例子的函数调用中,会真的产生一个对象吗?若是会,那大量的函数调用会白白生成不少有待 GC 释放的临时对象,那么就意味着在函数参数少时,仍是须要尽可能避免采用解构传参,而使用传统的:node
function f(a,b){}
f(1,2);
复制代码
上面的描述其实同时提了好几个问题:bash
首先从上面给的代码例子中,确实会产生一个对象。可是在实际项目中,有很大的几率是不须要产生这个临时对象的。函数
我以前写过一篇文章 使用 D8 分析 javascript 如何被 V8 引擎优化的。那么咱们就分析一下你的示例代码。性能
function f(a,b){
return a+b;
}
const d = f(1, 2);
复制代码
鉴于不少人没有 d8,所以咱们使用 node.js 代替。运行:测试
node --print-bytecode add.js
复制代码
其中的 --print-bytecode
能够查看 V8 引擎生成的字节码。在输出结果中查找 [generating bytecode for function: f]
:优化
[generating bytecode for function: ]
Parameter count 6
Frame size 32
0000003AC126862A @ 0 : 6e 00 00 02 CreateClosure [0], [0], #2
0000003AC126862E @ 4 : 1e fb Star r0
10 E> 0000003AC1268630 @ 6 : 91 StackCheck
98 S> 0000003AC1268631 @ 7 : 03 01 LdaSmi [1]
0000003AC1268633 @ 9 : 1e f9 Star r2
0000003AC1268635 @ 11 : 03 02 LdaSmi [2]
0000003AC1268637 @ 13 : 1e f8 Star r3
98 E> 0000003AC1268639 @ 15 : 51 fb f9 f8 01 CallUndefinedReceiver2 r0, r2, r3, [1]
0000003AC126863E @ 20 : 04 LdaUndefined
107 S> 0000003AC126863F @ 21 : 95 Return
Constant pool (size = 1)
Handler Table (size = 16)
[generating bytecode for function: f]
Parameter count 3
Frame size 0
72 E> 0000003AC1268A6A @ 0 : 91 StackCheck
83 S> 0000003AC1268A6B @ 1 : 1d 02 Ldar a1
91 E> 0000003AC1268A6D @ 3 : 2b 03 00 Add a0, [0]
94 S> 0000003AC1268A70 @ 6 : 95 Return
Constant pool (size = 0)
Handler Table (size = 16)
复制代码
Star r0
将当前在累加器中的值存储在寄存器 r0
中。ui
LdaSmi [1]
将小整数(Smi)1
加载到累加器寄存器中。spa
而函数体只有两行代码:Ldar a1
和 Add a0, [0]
。
当咱们使用解构赋值后:
[generating bytecode for function: ]
Parameter count 6
Frame size 24
000000D24A568662 @ 0 : 6e 00 00 02 CreateClosure [0], [0], #2
000000D24A568666 @ 4 : 1e fb Star r0
10 E> 000000D24A568668 @ 6 : 91 StackCheck
100 S> 000000D24A568669 @ 7 : 6c 01 03 29 f9 CreateObjectLiteral [1], [3], #41, r2
100 E> 000000D24A56866E @ 12 : 50 fb f9 01 CallUndefinedReceiver1 r0, r2, [1]
000000D24A568672 @ 16 : 04 LdaUndefined
115 S> 000000D24A568673 @ 17 : 95 Return
Constant pool (size = 2)
Handler Table (size = 16)
[generating bytecode for function: f]
Parameter count 2
Frame size 40
72 E> 000000D24A568AEA @ 0 : 91 StackCheck
000000D24A568AEB @ 1 : 1f 02 fb Mov a0, r0
000000D24A568AEE @ 4 : 1d fb Ldar r0
000000D24A568AF0 @ 6 : 89 06 JumpIfUndefined [6] (000000D24A568AF6 @ 12)
000000D24A568AF2 @ 8 : 1d fb Ldar r0
000000D24A568AF4 @ 10 : 88 10 JumpIfNotNull [16] (000000D24A568B04 @ 26)
000000D24A568AF6 @ 12 : 03 3f LdaSmi [63]
000000D24A568AF8 @ 14 : 1e f8 Star r3
000000D24A568AFA @ 16 : 09 00 LdaConstant [0]
000000D24A568AFC @ 18 : 1e f7 Star r4
000000D24A568AFE @ 20 : 53 e8 00 f8 02 CallRuntime [NewTypeError], r3-r4
74 E> 000000D24A568B03 @ 25 : 93 Throw
74 S> 000000D24A568B04 @ 26 : 20 fb 00 02 LdaNamedProperty r0, [0], [2]
000000D24A568B08 @ 30 : 1e fa Star r1
76 S> 000000D24A568B0A @ 32 : 20 fb 01 04 LdaNamedProperty r0, [1], [4]
000000D24A568B0E @ 36 : 1e f9 Star r2
85 S> 000000D24A568B10 @ 38 : 1d f9 Ldar r2
93 E> 000000D24A568B12 @ 40 : 2b fa 06 Add r1, [6]
96 S> 000000D24A568B15 @ 43 : 95 Return
Constant pool (size = 2)
Handler Table (size = 16)
复制代码
咱们能够看到,代码明显增长了不少,CreateObjectLiteral
建立了一个对象。原本只有 2 条核心指令的函数忽然增长到了近 20 条。其中不乏有 JumpIfUndefined
、CallRuntime
、Throw
这种指令。
因为这个内存占用很小,所以咱们加一个循环。
function f(a, b){
return a + b;
}
for (let i = 0; i < 1e8; i++) {
const d = f(1, 2);
}
console.log(%GetHeapUsage());
复制代码
%GetHeapUsage()
函数有些特殊,以百分号(%)开头,这个是 V8 引擎内部调试使用的函数,咱们能够经过命令行参数 --allow-natives-syntax
来使用这些函数。
node --trace-gc --allow-natives-syntax add.js
复制代码
获得结果(为了便于阅读,我调整了输出格式):
[10192:0000000000427F50]
26 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.3 / 0.0 ms allocation failure
[10192:0000000000427F50]
34 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.8 / 0.0 ms allocation failure
4424128
复制代码
当使用解构赋值后:
[7812:00000000004513E0]
27 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.0 / 0.0 ms allocation failure
[7812:00000000004513E0]
36 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.7 / 0.0 ms allocation failure
[7812:00000000004513E0]
56 ms: Scavenge 4.6 (8.3) -> 4.1 (11.3) MB, 0.5 / 0.0 ms allocation failure
4989872
复制代码
能够看到多了所以内存分配,并且堆空间的使用也比以前多了。使用 --trace_gc_verbose
参数能够查看 gc 更详细的信息,还能够看到这些内存都是新生代,清理起来的开销仍是比较小的。
经过逃逸分析,V8 引擎能够把临时对象去除。
还考虑以前的函数:
function add({a, b}){
return a + b;
}
复制代码
若是咱们还有一个函数,double
,用于给一个数字加倍。
function double(x) {
return add({a:x, b:x});
}
复制代码
而这个 double
函数最终会被编译为
function double(x){
return x + x;
}
复制代码
在 V8 引擎内部,会按照以下步骤进行逃逸分析处理:
首先,增长中间变量:
function add(o){
return o.a + o.b;
}
function double(x) {
let o = {a:x, b:x};
return add(o);
}
复制代码
把对函数 add
的调用进行内联展开,变成:
function double(x) {
let o = {a:x, b:x};
return o.a + o.b;
}
复制代码
替换对字段的访问操做:
function double(x) {
let o = {a:x, b:x};
return x + x;
}
复制代码
删除没有使用到的内存分配:
function double(x) {
return x + x;
}
复制代码
经过 V8 的逃逸分析,把原本分配到堆上的对象去除了。
不要作这种语法层面的微优化,引擎会去优化的,业务代码仍是更加关注可读性和可维护性。若是你写的是库代码,能够尝试这种优化,把参数展开后直接传递,到底能带来多少性能收益还得看最终的基准测试。
举个例子就是 Chrome 49 开始支持 Proxy
,直到一年以后的 Chrome 62 才改进了 Proxy
的性能,使 Proxy
的总体性能提高了 24% ~ 546%。
(PS:对象的解构赋值不是 ES6(ES2015) 的新特性,而是 ES9(ES2018) 的)