ES6 的解构赋值前每次都创建一个对象吗?会加重 GC 的负担吗?

本文来源于知乎上的一个提问。

为了程序的易读性,我们会使用 ES6 的解构赋值:

 
 
  1. function f({a,b}){}

  2. f({a:1,b:2});

这个例子的函数调用中,会真的产生一个对象吗?如果会,那大量的函数调用会白白生成很多有待 GC 释放的临时对象,那么就意味着在函数参数少时,还是需要尽量避免采用解构传参,而使用传统的:

 
 
  1. function f(a,b){}

  2. f(1,2);

上面的描述其实同时提了好几个问题:

  1. 会不会产生一个对象?

  2. 参数少时,是否需要尽量避免采用解构传参?

  3. 对性能(CPU/内存)的影响多大?

1. 从 V8 字节码分析两者的性能表现

首先从上面给的代码例子中,确实会产生一个对象。但是在实际项目中,有很大的概率是不需要产生这个临时对象的。

我之前写过一篇文章 使用 D8 分析 javascript 如何被 V8 引擎优化的。那么我们就分析一下你的示例代码。

 
 
  1. function f(a,b){

  2. return a+b;

  3. }

  4. const d = f(1, 2);

鉴于很多人没有 d8,因此我们使用 node.js 代替。运行:

 
 
  1. node --print-bytecode add.js

其中的 --print-bytecode 可以查看 V8 引擎生成的字节码。在输出结果中查找 [generating bytecodeforfunction:f]

 
 
  1. [generating bytecode for function: ]

  2. Parameter count 6

  3. Frame size 32

  4.         0000003AC126862A @    0 : 6e 00 00 02       CreateClosure [0], [0], #2

  5.         0000003AC126862E @    4 : 1e fb             Star r0

  6.   10 E> 0000003AC1268630 @    6 : 91                StackCheck

  7.   98 S> 0000003AC1268631 @    7 : 03 01             LdaSmi [1]

  8.         0000003AC1268633 @    9 : 1e f9             Star r2

  9.         0000003AC1268635 @   11 : 03 02             LdaSmi [2]

  10.         0000003AC1268637 @   13 : 1e f8             Star r3

  11.   98 E> 0000003AC1268639 @   15 : 51 fb f9 f8 01    CallUndefinedReceiver2 r0, r2, r3, [1]

  12.         0000003AC126863E @   20 : 04                LdaUndefined

  13.  107 S> 0000003AC126863F @   21 : 95                Return

  14. Constant pool (size = 1)

  15. Handler Table (size = 16)

  16. [generating bytecode for function: f]

  17. Parameter count 3

  18. Frame size 0

  19.   72 E> 0000003AC1268A6A @    0 : 91                StackCheck

  20.   83 S> 0000003AC1268A6B @    1 : 1d 02             Ldar a1

  21.   91 E> 0000003AC1268A6D @    3 : 2b 03 00          Add a0, [0]

  22.   94 S> 0000003AC1268A70 @    6 : 95                Return

  23. Constant pool (size = 0)

  24. Handler Table (size = 16)

Starr0 将当前在累加器中的值存储在寄存器 r0 中。

LdaSmi[1] 将小整数(Smi) 1 加载到累加器寄存器中。

而函数体只有两行代码: Ldara1 和 Add a0 ,[0]

当我们使用解构赋值后:

 
 
  1. [generating bytecode for function: ]

  2. Parameter count 6

  3. Frame size 24

  4.         000000D24A568662 @    0 : 6e 00 00 02       CreateClosure [0], [0], #2

  5.         000000D24A568666 @    4 : 1e fb             Star r0

  6.   10 E> 000000D24A568668 @    6 : 91                StackCheck

  7.  100 S> 000000D24A568669 @    7 : 6c 01 03 29 f9    CreateObjectLiteral [1], [3], #41, r2

  8.  100 E> 000000D24A56866E @   12 : 50 fb f9 01       CallUndefinedReceiver1 r0, r2, [1]

  9.         000000D24A568672 @   16 : 04                LdaUndefined

  10.  115 S> 000000D24A568673 @   17 : 95                Return

  11. Constant pool (size = 2)

  12. Handler Table (size = 16)

  13. [generating bytecode for function: f]

  14. Parameter count 2

  15. Frame size 40

  16.   72 E> 000000D24A568AEA @    0 : 91                StackCheck

  17.         000000D24A568AEB @    1 : 1f 02 fb          Mov a0, r0

  18.         000000D24A568AEE @    4 : 1d fb             Ldar r0

  19.         000000D24A568AF0 @    6 : 89 06             JumpIfUndefined [6] (000000D24A568AF6 @ 12)

  20.         000000D24A568AF2 @    8 : 1d fb             Ldar r0

  21.         000000D24A568AF4 @   10 : 88 10             JumpIfNotNull [16] (000000D24A568B04 @ 26)

  22.         000000D24A568AF6 @   12 : 03 3f             LdaSmi [63]

  23.         000000D24A568AF8 @   14 : 1e f8             Star r3

  24.         000000D24A568AFA @   16 : 09 00             LdaConstant [0]

  25.         000000D24A568AFC @   18 : 1e f7             Star r4

  26.         000000D24A568AFE @   20 : 53 e8 00 f8 02    CallRuntime [NewTypeError], r3-r4

  27.   74 E> 000000D24A568B03 @   25 : 93                Throw

  28.   74 S> 000000D24A568B04 @   26 : 20 fb 00 02       LdaNamedProperty r0, [0], [2]

  29.         000000D24A568B08 @   30 : 1e fa             Star r1

  30.   76 S> 000000D24A568B0A @   32 : 20 fb 01 04       LdaNamedProperty r0, [1], [4]

  31.         000000D24A568B0E @   36 : 1e f9             Star r2

  32.   85 S> 000000D24A568B10 @   38 : 1d f9             Ldar r2

  33.   93 E> 000000D24A568B12 @   40 : 2b fa 06          Add r1, [6]

  34.   96 S> 000000D24A568B15 @   43 : 95                Return

  35. Constant pool (size = 2)

  36. Handler Table (size = 16)

我们可以看到,代码明显增加了很多, CreateObjectLiteral 创建了一个对象。本来只有 2 条核心指令的函数突然增加到了近 20 条。其中不乏有 JumpIfUndefined、 CallRuntime 、 Throw 这种指令。

2. 使用 --trace-gc 参数查看内存

由于这个内存占用很小,因此我们加一个循环。

 
 
  1. function f(a, b){

  2. return a + b;

  3. }

  4. for (let i = 0; i < 1e8; i++) {

  5. const d = f(1, 2);

  6. }

  7. console.log(%GetHeapUsage());

%GetHeapUsage() 函数有些特殊,以百分号(%)开头,这个是 V8 引擎内部调试使用的函数,我们可以通过命令行参数 --allow-natives-syntax 来使用这些函数。

 
 
  1. node --trace-gc --allow-natives-syntax add.js

得到结果(为了便于阅读,我调整了输出格式):

 
 
  1. [10192:0000000000427F50]

  2. 26 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.3 / 0.0 ms  allocation failure

  3. [10192:0000000000427F50]

  4. 34 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.8 / 0.0 ms  allocation failure

  5. 4424128

当使用解构赋值后:

 
 
  1. [7812:00000000004513E0]

  2. 27 ms: Scavenge 3.4 (6.3) -> 3.1 (7.3) MB, 1.0 / 0.0 ms  allocation failure

  3. [7812:00000000004513E0]

  4. 36 ms: Scavenge 3.6 (7.3) -> 3.5 (8.3) MB, 0.7 / 0.0 ms  allocation failure

  5. [7812:00000000004513E0]

  6. 56 ms: Scavenge 4.6 (8.3) -> 4.1 (11.3) MB, 0.5 / 0.0 ms  allocation failure

  7. 4989872

可以看到多了因此内存分配,而且堆空间的使用也比之前多了。使用 --trace_gc_verbose 参数可以查看 gc 更详细的信息,还可以看到这些内存都是新生代,清理起来的开销还是比较小的。

3. Escape Analysis 逃逸分析

通过逃逸分析,V8 引擎可以把临时对象去除。

还考虑之前的函数:

 
 
  1. function add({a, b}){

  2.   return a + b;

  3. }

如果我们还有一个函数, double,用于给一个数字加倍。

 
 
  1. function double(x) {

  2.   return add({a:x, b:x});

  3. }

而这个 double 函数最终会被编译为

 
 
  1. function double(x){

  2.    return x + x;

  3. }

在 V8 引擎内部,会按照如下步骤进行逃逸分析处理:

首先,增加中间变量:

 
 
  1. function add(o){

  2. return o.a + o.b;

  3. }

  4. function double(x) {

  5.   let o = {a:x, b:x};

  6.   return add(o);

  7. }

把对函数 add 的调用进行内联展开,变成:

 
 
  1. function double(x) {

  2.   let o = {a:x, b:x};

  3.   return o.a + o.b;

  4. }

替换对字段的访问操作:

 
 
  1. function double(x) {

  2.   let o = {a:x, b:x};

  3.   return x + x;

  4. }

删除没有使用到的内存分配:

 
 
  1. function double(x) {

  2.   return x + x;

  3. }

通过 V8 的逃逸分析,把本来分配到堆上的对象去除了。

4. 结论

不要做这种语法层面的微优化,引擎会去优化的,业务代码还是更加关注可读性和可维护性。如果你写的是库代码,可以尝试这种优化,把参数展开后直接传递,到底能带来多少性能收益还得看最终的基准测试。

举个例子就是 Chrome 49 开始支持 Proxy,直到一年之后的 Chrome 62 才改进了 Proxy 的性能,使 Proxy 的整体性能提升了 24% ~ 546%。

(PS:对象的解构赋值不是 ES6(ES2015) 的新特性,而是 ES9(ES2018) 的)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值