JavaScript V8 性能小貼士

jopen 8年前發布 | 23K 次閱讀 JavaScript開發 JavaScript

原文:Performance Tips for JavaScript in V8
譯文:JavaScript V8性能小貼士


簡介

關于如何巧妙提高 V8 JavaScript 性能的話題,Daniel Clifford 在 Google I/O 上做了一次非常精彩的分享。Daniel 鼓勵我們“追求更快”,認真的分析 C++ 和 JavaScript 之間的性能差距,根據 JavaScript 的工作原理撰寫代碼。在 Daniel 的分享中,有一個核心要點的歸納,我們也會根據性能指導的變化保持對這篇文章的更新。

最重要的建議

最重要的是要把任何性能建議放在特定的情境當中。性能建議是附加的東西,有時一開始就特別注意深層的建議反而會對我們造成干擾。你需要從一個綜合的角度看待你的 Web 應用的性能——在關注這些性能建議之前,你應該找 PageSpeed 之類的工具大概分析一下你的代碼,也算是跑個分先。這會防止你過度優化。

對Web應用的性能優化,幾個原則性的建議是:

  • 首先,未雨綢繆
  • 然后,找到癥結
  • 最后,修復它

為了完成這幾個步驟,理解 V8 如何優化 JS 是一件很重要的事情,這樣你就可以根據其對 JS 運行時的設計撰寫代碼。同樣重要的是掌握一些幫得上忙的工具。Daniel 也交代了一些開發者工具的用法,它們剛好抓住了一些V8引擎設計上最重要的部分。

OK。開始 V8 小貼士。

隱藏類

JavaScript 限制編譯時的類型信息:類型可以在運行時被改變,可想而知這導致 JS 類型在編譯時代價昂貴。那么你一定會問:JavaScript 的性能有機會和 C++ 相提并論嗎?盡管如此,V8 在運行時隱藏了內部創建對象的類型,隱藏類相同的對象可以使用相同的生成碼以達到優化的目的。

比如:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// 這里的 p1 和 p2 擁有共享的隱藏類
p2.z = 55;
// 注意!這時 p1 和 p2 的隱藏類已經不同了!

在我們為p2添加z這個成員之前,p1和p2一直共享相同的內部隱藏類——所以 V8 可以生成一段單獨版本的優化匯編碼,這段代碼可以同時封裝p1和p2的 JavaScript 代碼。我們越避免隱藏類的派生,就會獲得越高的性能。

結論

  • 在構造函數里初始化所有對象的成員(所以這些實例之后不會改變其隱藏類)
  • 總是以相同的次序初始化對象成員

數字

當類型可以改變時,V8 使用標記來高效的標識其值。V8 通過其值來推斷你會以什么類型的數字來對待它。因為這些類型可以動態改變,所以一旦 V8 完成了推斷,就會通過標記高效完成值的標識。不過有的時候改變類型標記還是比較消耗性能的,我們最好保持數字的類型始終不變。通常標識為有符號的 31 位整數是最優的。

比如:

var i = 42; // 這是一個31位有符號整數
var j = 4.2; // 這是一個雙精度浮點數

結論

盡量使用可以用 31 位有符號整數表示的數。

數組

為了掌控大而稀疏的數組,V8 內部有兩種數組存儲方式:

  • 快速元素:對于緊湊型關鍵字集合,進行線性存儲
  • 字典元素:對于其它情況,使用哈希表

最好別導致數組存儲方式在兩者之間切換。

結論

  • 使用從 0 開始連續的數組關鍵字
  • 別預分配大數組(比如大于 64K 個元素)到其最大尺寸,令其尺寸順其自然發展就好
  • 別刪除數組里的元素,尤其是數字數組
  • 別加載未初始化或已刪除的元素: ```javascript a = new Array(); for (var b = 0; b < 10; b++) { a[0] |= b; // 杯具! }

// vs.

a = new Array(); a[0] = 0; for (var b = 0; b < 10; b++) { a[0] |= b; // 比上面快 2 倍 } ```

同樣的,雙精度數組會更快——數組的隱藏類會根據元素類型而定,而只包含雙精度的數組會被拆箱(unbox),這導致隱藏類的變化。對數組不經意的封裝就可能因為裝箱/拆箱(boxing/unboxing)而導致額外的開銷。比如:

var a = new Array();
a[0] = 77; // 分配
a[1] = 88;
a[2] = 0.5; // 分配,轉換
a[3] = true; // 分配,轉換

下面的寫法效率更高:

var a = [77, 88, 0.5, true]; 

因為第一個例子是一個一個分配賦值的,并且對a[2]的賦值導致數組被拆箱為了雙精度。但是對a[3]的賦值又將數組重新裝箱回了任意值(數字或對象)。第二種寫法時,編譯器一次性知道了所有元素的字面上的類型,隱藏隱藏類可以直接確定。

結論

  • 初始化小額定長數組時,用字面量進行初始化
  • 小數組(小于 64k)在使用之前先預分配正確的尺寸
  • 請勿在數字數組中存放非數字的值(對象)
  • 如果通過非字面量進行初始化小數組時,切勿觸發類型的重新轉換

JavaScript 編譯

盡管 JavaScript 是個非常動態的語言,且原本的實現是解釋性的,但現代的 JavaScript 運行時引擎都會進行編譯。V8(Chrome 的 JavaScript)有兩個不同的運行時(JIT)編譯器:

  • “完全”編譯器,可以為任何 JavaScript 生成優秀的代碼
  • 優化編譯器,可以為大部分 JavaScript 生成偉大(汗一下自己的翻譯)的代碼,但會更耗時。

完全編譯器

在 V8 中,完全編譯器會以最快的速度運行在任何代碼上,快速生成優秀但不偉大的代碼。該編譯器在編譯時幾乎不做任何有關類型的假設——它預測類型在運行時會發生改變。完全編譯器的生成碼通過內聯緩存(ICs)在程序運行時提煉類型相關的知識,以便將來改進和優化。

內聯緩存的目的是,通過緩存依賴類型的代碼進行操作,更有效率的掌控類型。當代碼運行時,它會先驗證對類型的假設,然后使用內聯緩存快速執行操作。這也意味著可以接受多種類型的操作會變得效率低下。

結論

  • 單態操作優于多態操作

如果一個操作的輸入總是相同類型的,則其為單態操作。否則,操作調用時的某個參數可以跨越不同的類型,那就是多態操作。比如add()的第二個調用就觸發了多態操作:

function add(x, y) { return x + y; }

add(1, 2); // add 中的 + 操作是單態操作 add("a", "b"); // add 中的 + 操作變成了多態操作

優化編譯器

V8 有一個和完全編譯器并行的優化編譯器,它會重編那些最“熱門”(即被調用多次)的函數。優化編譯器通過類型反饋來使得編譯過的代碼更快——事實上它就是使用了我們之前談到的 ICs 的類型信息!

在優化編譯器里,操作都是內聯的(直接出現在被調用的地方)。它加速了執行(拿內存空間換來的),同時也進行了各種優化。單態操作的函數和構造函數可以整個內聯起來(這是 V8 中單態操作的有一個好處)。

你可以使用單獨的“d8”版本的 V8 引擎來獲取優化記錄:

d8 --trace-opt primes.js

(其會把被優化的函數名輸出出來)

不是所有的函數都可以被優化,有些特性會阻止優化編譯器運行一個已知函數(bail-out)。目前優化編譯器會排除有 try/catch 的代碼塊的函數。

結論

  • 如果存在 try/catch 代碼快,則將性能敏感的代碼放到一個嵌套的函數中: ```javascript function perf_sentitive() { // 把性能敏感的工作放置于此 }

try { perf_sentitive() } catch (e) { // 在此處理異常 } ```

這個建議可能會在未來發生改變,因為我們會在優化編譯器里開啟 try/catch 代碼塊。你可以通過使用上述的 d8 選項--trace-opt得到更多有關這些函數的信息來檢驗優化編譯器如何排除這些函數。

d8 --trace-opt primes.js

取消優化

最終,編譯器的性能優化是有針對性的——有時它的變現并不好,我們就不得不回退。“取消優化”的過程實際上就是把優化過的代碼扔掉,恢復執行完全編譯器的代碼。重優化可能稍后再打開,但是短期內性能會下降。尤其是取消優化的發生會導致其函數的變量的隱藏類的變化。

結論

  • 回避在優化過后函數內隱藏類改變

你可以像其它優化一樣,通過 V8 的一個日志標識來取消優化。

d8 --trace-deopt primes.js

其它V8工具

順便提一下,你還可以在Chrome啟動時傳遞V8跟蹤選項:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"

額外使用開發者工具分析,你可以使用 d8 進行分析:

% out/ia32.release/d8 primes.js --prof

它通過內建的采樣分析器,對每毫秒進行采樣,并寫入 v8.log。

回到摘要……

重要的是認識和理解 V8 引擎如何處理你的代碼,進而為優化 JavaScript 做好準備。再次強調我們的基礎建議:

  • 首先,未雨綢繆
  • 然后,找到癥結
  • 最后,修復它

這意味著你應該通過 PageSpeed 之類的工具先確定你的 JavaScript 中的問題,在收集指標之前盡可能減少至純粹的 JavaScript(沒有 DOM),然后通過指標來定位瓶頸所在,評估重要程度。希望 Daniel 的分享會幫助你更好的理解V8如何運行 JavaScript ——但是也要確保專注于優化你自身的算法!

參考資料

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!