天天看點

V8 性能優化殺手V8 性能優化殺手

<b>本文講的是V8 性能優化殺手,</b>

<b></b>

這篇文章将會給你一些建議,讓你避免寫出性能遠低于期望值的代碼。在此特别指出有一些代碼會導緻 V8 引擎(涉及到 Node.JS、Opera、Chromium 等)無法對相關函數進行優化。

V8 引擎中沒有解釋器,但有 2 種不同的編譯器:普通編譯器與優化編譯器。編譯器會将你的 JavaScript 代碼編譯成彙編語言後直接運作。但這并不意味着運作速度會很快。被編譯成彙編語言後的代碼并不能顯著地提高其性能,它隻能省去解釋器的性能開銷,如果你的代碼沒有被優化的話速度依然會很慢。

例如,在普通編譯器中 <code>a + b</code> 将會被編譯成下面這樣:

換句話說,其實它僅僅調用了 runtime 函數。但如果 <code>a</code> 和 <code>b</code> 能确定都是整型變量,那麼編譯結果會是下面這樣:

它的執行速度會比前面那種去在 runtime 中調用複雜的 JavaScript 加法算法快得多。

通常來說,使用普通編譯器将會得到前面那種代碼,使用優化編譯器将會得到後面那種代碼。走優化編譯器的代碼可以說比走普通編譯器的代碼性能好上 100 倍。但是請注意,并不是任何類型的 JavaScript 代碼都能被優化。在 JS 中,有很多種情況(甚至包括一些我們常用的文法)是不能被優化編譯器優化的(這種情況被稱為“bailout”,從優化編譯器降級到普通編譯器)。

記住一些會導緻整個函數無法被優化的情況是很重要的。JS 代碼被優化時,将會逐個優化函數,在優化各個函數的時候不會關心其它的代碼做了什麼(除非那些代碼被内聯在即将優化的函數中。)。

這篇文章涵蓋了大多數會導緻函數墜入“無法被優化的深淵”的情況。不過在未來,優化編譯器進行更新後能夠識别越來越多的情況時,下面給出的建議與各種變通方法可能也會變的不再必要或者需要修改。

<a href="https://juejin.im/post/5959edfc5188250d83241399#1-%E5%B7%A5%E5%85%B7" target="_blank">工具</a>

<a href="https://juejin.im/post/5959edfc5188250d83241399#2-%E4%B8%8D%E6%94%AF%E6%8C%81%E7%9A%84%E8%AF%AD%E6%B3%95" target="_blank">不支援的文法</a>

<a href="https://juejin.im/post/5959edfc5188250d83241399#3-%E4%BD%BF%E7%94%A8-arguments" target="_blank">使用 <code>arguments</code></a>

<a href="https://juejin.im/post/5959edfc5188250d83241399#4-switch-case" target="_blank">Switch-case</a>

<a href="https://juejin.im/post/5959edfc5188250d83241399#5-for-in" target="_blank">For-in</a>

<a href="https://juejin.im/post/5959edfc5188250d83241399#6-%E9%80%80%E5%87%BA%E6%9D%A1%E4%BB%B6%E8%97%8F%E7%9A%84%E5%BE%88%E6%B7%B1-%E6%88%96%E8%80%85%E6%B2%A1%E6%9C%89%E5%AE%9A%E4%B9%89%E6%98%8E%E7%A1%AE%E5%87%BA%E5%8F%A3%E7%9A%84%E6%97%A0%E9%99%90%E5%BE%AA%E7%8E%AF" target="_blank">退出條件藏的很深,或者沒有定義明确出口的無限循環</a>

你可以在 node.js 中使用一些 V8 自帶的标記來驗證不同的代碼用法對優化的影響。通常來說你可以建立一個包括特定模式的函數,然後使用所有允許的參數類型去調用它,再使用 V8 的内部去優化與檢查它:

test.js:

運作它:

<a href="https://link.juejin.im/?target=https%3A%2F%2Fcodereview.chromium.org%2F1962103003" target="_blank">codereview.chromium.org/1962103003</a>

為了檢驗我們做的這個工具是否真的有用,注釋掉 <code>eval</code> 語句然後再運作一次:

事實證明,使用這個工具來驗證處理方法是可行且必要的。

有一些文法結構是不支援被編譯器優化的,用這類文法将會導緻包含在其中的函數不能被優化。

請注意,即使這些語句不會被通路到或者不會被執行,它仍然會導緻整個函數不能被優化。

例如下面這樣做是沒用的:

即使 debugger 語句根本不會被執行到,上面的代碼将會導緻包含它的整個函數都不能被優化。

目前不可被優化的文法有:

包含 <code>const</code> 複合指派的函數 (Chrome 56 / V8 5.6! 對其做了優化)

包含 <code>__proto__</code> 對象字面量、<code>get</code> 聲明、<code>set</code> 聲明的函數

看起來永遠不會被優化的文法有:

包含 <code>debugger</code> 語句的函數

包含字面調用 <code>eval()</code> 的函數

包含 <code>with</code> 語句的函數

最後明确一下:如果你用了下面任何一種情況,整個函數将不能被優化:

另外在此要特别提一下 <code>eval</code> 和 <code>with</code>,它們會導緻它們的調用棧鍊變成動态作用域,可能會導緻其它的函數也受到影響,因為這種情況無法從字面上判斷各個變量的有效範圍。

變通辦法

前面提到的不能被優化的語句用在生産環境代碼中是無法避免的,例如 <code>try-finally</code> 和 <code>try-catch</code>。為了讓使用這些語句的影響盡量減小,它們需要被隔離在一個最小化的函數中,這樣主要的函數就不會被影響:

有許多種使用 <code>arguments</code> 的方式會導緻函數不能被優化。是以當使用 <code>arguments</code> 的時候需要格外小心。

變通方法 是将參數值儲存在一個新的變量中:

如果僅僅是像上面這樣用 <code>arguments</code>(上面代碼作用為檢測第二個參數是否存在,如果不存在則指派為 5),也可以用 <code>undefined</code> 檢測來代替這段代碼:

但是之後如果需要用到 <code>arguments</code>,很容易忘記需要在這兒加上重新指派的語句。

變通方法 2:為整個檔案或者整個函數開啟嚴格模式 (<code>'use strict'</code>)。

<code>arguments</code> 對象在任何地方都不允許被傳遞或者被洩露。

變通方法 可以通過建立一個數組來代理 <code>arguments</code> 對象:

但是這樣要寫很多讓人煩的代碼,是以得判斷是否真的值得這麼做。後面一次又一次的優化會代理更多的代碼,越來越多的代碼意味着代碼本身的意義會被逐漸淹沒。

不過,如果你有 build 這個過程,可以将上面這一系列過程由一個不需要 source map 的宏來實作,保證代碼為合法的 JavaScript:

Bluebird 就使用了這個技術,上面的代碼經過 build 之後會被拓展成下面這樣:

在非嚴格模式下可以這麼做:

變通方法:犯不着寫這麼蠢的代碼。另外,在嚴格模式下它會報錯。

隻使用:

<code>arguments.length</code>

<code>arguments[i]</code> <code>i</code> 需要始終為 arguments 的合法整型索引,且不允許越界

除了 <code>.length</code> 和 <code>[i]</code>,不要直接使用 <code>arguments</code>

嚴格來說用 <code>fn.apply(y, arguments)</code> 是沒問題的,但除此之外都不行(例如 <code>.slice</code>)。<code>Function#apply</code> 是特别的存在。

請注意,給函數添加屬性值(例如 <code>fn.$inject = ...</code>)和綁定函數(即 <code>Function#bind</code>的結果)會生成隐藏類,是以此時使用 <code>#apply</code> 不安全。

如果你按照上面的安全方式做,毋需擔心使用 <code>arguments</code> 導緻不确定 arguments 對象的配置設定。

在以前,一個 switch-case 語句最多隻能包含 128 個 case 代碼塊,超過這個限制的 switch-case 語句以及包含這種語句的函數将不能被優化。

你需要讓 case 代碼塊的數量保持在 128 個之内,否則應使用函數數組或者 if-else。

For-in 語句在某些情況下會導緻整個函數無法被優化。

這也解釋了”For-in 速度不快“之類的說法。

這兩種用法db都将會導緻函數不能被優化的問題。是以鍵不能在上級作用域定義,也不能在下級作用域被引用。它必須是一個局部變量。

如果你給一個對象動态增加了很多的屬性(在構造函數外)、<code>delete</code> 屬性或者使用不合法的辨別符作為屬性,這個對象将會變成哈希表模式。換句話說,當你把一個對象當做哈希表來用,它就真的會變成哈希表。請不要對這種對象使用 <code>for-in</code>。你可以用過開啟 Node.JS 的<code>--allow-natives-syntax</code>,調用 <code>console.log(%HasFastProperties(obj))</code> 來判斷一個對象是否為哈希表模式。

上面這麼做會給所有對象(除了用 <code>Object.create(null)</code> 建立的對象)的原型鍊中添加一個可枚舉屬性。此時任何包含了 <code>for-in</code> 文法的函數都不會被優化(除非僅周遊<code>Object.create(null)</code> 建立的對象)。

你可以使用 <code>Object.defineProperty</code> 建立不可枚舉屬性(不推薦在 runtime 中調用,但是在定義一些例如原型屬性之類的靜态資料的時候它很高效)。

數組對象會給予一些種類的屬性名特殊待遇。對一個屬性名 P(字元串形式),當且僅當 ToString(ToUint32(P)) 等于 P 并且 ToUint32(P) 不等于 232−1 時,它是個 數組索引 。一個屬性名是數組索引的屬性也叫做元素 。

一般隻有數組有數組索引,但是有時候一般的對象也可能擁有數組索引: <code>normalObj[0] = value;</code>

是以使用 <code>for-in</code> 進行數組周遊不僅會比 for 循環要慢,還會導緻整個包含 <code>for-in</code> 語句的函數不能被優化。

如果你試圖使用 <code>for-in</code> 周遊一個非簡單可枚舉對象,它會導緻包含它的整個函數不能被優化。

變通方法:隻對 <code>Object.keys</code> 使用 <code>for-in</code>,如果要周遊數組需使用 for 循環。如果非要周遊整個原型鍊上的屬性,需要将 <code>for-in</code> 隔離在一個輔助函數中以降低影響:

有時候在你寫代碼的時候,你需要用到循環,但是不确定循環體内的代碼之後會是什麼樣子。是以這時候你用了一個 <code>while (true) {</code> 或者 <code>for (;;) {</code>,在之後将終止條件放在循環體中,打斷循環進行後面的代碼。然而你寫完這些之後就忘了這回事。在重構時,你發現這個函數很慢,出現了反優化情況 - 上面的循環很可能就是罪魁禍首。

重構時将循環内的退出條件放到循環的條件部分并不是那麼簡單。

如果代碼中的退出條件是循環最後的 if 語句的一部分,且代碼至少要運作一輪,那麼你可以将這個循環重構為 <code>do{} while ();</code>。

如果退出條件在循環的開頭,請将它放在循環的條件部分中去。

如果退出條件在循環體中部,你可以嘗試”滾動“代碼:試着依次将一部分退出條件前的代碼移到後面去,然後在之前的位置留下它的引用。當退出條件可以放在循環條件部分,或者至少變成一個淺顯的邏輯判斷時,這個循環就不再會出現反優化的情況了。

<b>原文釋出時間為:2017年7月3日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀