本文來源於知乎上的一個提問。javascript
爲了程序的易讀性,咱們會使用 ES6 的解構賦值:java
function f({a,b}){} f({a:1,b:2});
這個例子的函數調用中,會真的產生一個對象嗎?若是會,那大量的函數調用會白白生成不少有待 GC 釋放的臨時對象,那麼就意味着在函數參數少時,仍是須要儘可能避免採用解構傳參,而使用傳統的:node
function f(a,b){} f(1,2);
上面的描述其實同時提了好幾個問題:函數
首先從上面給的代碼例子中,確實會產生一個對象。可是在實際項目中,有很大的機率是不須要產生這個臨時對象的。性能
我以前寫過一篇文章 使用 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
中。調試
LdaSmi [1]
將小整數(Smi)1
加載到累加器寄存器中。code
而函數體只有兩行代碼: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%。