This article comes from a question on Zhihu.

For legibility, we’ll use ES6’s destruct assignment:

function f({a,b}){}
f({a:1.b:2});
Copy the code

Does the function call in this example actually produce an object? If so, then a large number of function calls will generate a lot of temporary objects to be released by GC, which means that when a function has few arguments, it is necessary to avoid using deconstructed arguments and use traditional ones instead:

function f(a,b){}
f(1.2);
Copy the code

The above description raises several questions at the same time:

  1. Will it produce an object?
  2. When parameters are small, do you need to avoid using deconstructed parameters?
  3. What is the impact on performance (CPU/ memory)?

1. Analyze their performance from V8 bytecode

First, from the code example given above, an object is actually generated. In real projects, however, there is a high probability that this temporary object will not need to be generated.

I previously wrote an article using D8 to analyze how javascript is optimized for the V8 engine. So let’s look at your sample code.

function f(a,b){
 return a+b;
}

const d = f(1.2);
Copy the code

Since many people don’t have D8, we’ll use Node.js instead. Run:

node --print-bytecode add.js
Copy the code

Where –print-bytecode can view the bytecode generated by the V8 engine. [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)
Copy the code

Star r0 stores the value currently in the accumulator in register R0.

LdaSmi [1] loads the small integer (Smi) 1 into the accumulator register.

The function body has only two lines of code: Ldar A1 and Add A0, [0].

When we use destruct assignment:

[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)
Copy the code

As you can see, the code has obviously increased a lot. CreateObjectLiteral creates an object. A function that had only two core instructions suddenly had nearly 20. There are directives like JumpIfUndefined, CallRuntime, and Throw.

  • Read More: Understanding V8 bytecode “translation”

2. Use the –trace-gc parameter to check memory

Since this memory footprint is small, we add a loop.

function f(a, b){
 return a + b;
}

for (let i = 0; i < 1e8; i++) {
 const d = f(1.2);
}

console.log(%GetHeapUsage());
Copy the code

The %GetHeapUsage() function is somewhat special, starting with a percent sign (%). This is used for internal debugging of the V8 engine, and can be used with the command line parameter –allow-natives-syntax.

node --trace-gc --allow-natives-syntax add.js
Copy the code

The result (I’ve formatted the output to make it easier to read) :

[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
Copy the code

When using destruct assignment:

[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
Copy the code

You can see that there’s a lot more memory allocated, and there’s a lot more heap space being used. You can use the –trace_gc_verbose parameter to see more details about GC, and you can also see that this memory is of a new generation and that cleaning up is relatively inexpensive.

Escape Analysis

Through escape analysis, the V8 engine can remove temporary objects.

Also consider the previous function:

function add({a, b}){
   return a + b;
}
Copy the code

If we also have a function, double, that doubles a number.

function double(x) {
   return add({a:x, b:x});
}
Copy the code

And the double function will eventually compile to

function double(x){
    return x + x;
}
Copy the code

Inside the V8 engine, escape analysis is performed as follows:

First, add intermediate variables:

function add(o){
 return o.a + o.b;
}

function double(x) {
   let o = {a:x, b:x};
   return add(o);
}
Copy the code

Expand the call to add inline to read:

function double(x) {
   let o = {a:x, b:x};
   return o.a + o.b;
}
Copy the code

Replace access to fields:

function double(x) {
   let o = {a:x, b:x};
   return x + x;
}
Copy the code

Delete unused memory allocation:

function double(x) {
   return x + x;
}
Copy the code

Objects allocated to the heap are removed using V8’s escape analysis.

4. Conclusion

Don’t do syntactic micro-optimizations, engines will do that, business code is more about readability and maintainability. If you are writing library code, you can try this optimization, and just pass the parameters in. How much performance benefit will depend on the final benchmark.

For example, Chrome 49 started supporting Proxy, and it took Chrome 62 a year later to improve Proxy performance by 24% to 546% overall.

(PS: Deconstructed object assignment is not a new feature in ES6(ES2015), but in ES9(ES2018))