天天看點

從 V8 優化看高效 JavaScript

文本翻譯自: https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency

了解 JavaScript 是如何工作的對于編寫高效的 JS 大有幫助。

V8 執行 JS 分為三個階段:

  • 源代碼轉換為 AST 抽象文法樹。
  • 文法樹轉換為位元組碼:這個過程由 V8 的 Ignition 完成,2017年之前是沒有的。
  • 位元組碼編譯成機器碼:由 V8 的編譯器 TurboFan 來完成。

第一個階段并不是文本的讨論範圍,第二三階段對于編寫優化 JS 有直接影響。

實際上第二三階段是緊耦合的,它們都在 just-in-time( JIT )内運作。為了了解 JIT ,我們先回顧下源代碼轉換為機器碼的兩種方法:

1、解釋器

解釋器逐行轉換和執行代碼,其優點是易于實作和了解、及時回報、更寬泛的程式設計環境,缺點也非常明顯,那就是速度慢,慢的原因在于(1)反複解釋的開銷和(2)無法優化程式的各個部分。

換句話說,解釋器在處理不同的代碼段時無法識别重複的工作量。如果你通過解釋器運作相同的代碼 100 次,那麼解釋器将會翻譯并執行相同的代碼 100 次,其中不必要的重新翻譯了 99 次。

解釋器很簡單、啟動快速,但執行速度慢。

2、編譯器

編譯器在執行之前翻譯所有的源代碼。編譯器更加複雜,但是可以進行全局優化(例如,共享重複代碼),其執行速度也更快。

編譯器更複雜、啟動慢,但執行速度更快。

JIT 的作用就是盡可能結合解釋器和編譯器的優點,以使翻譯代碼和執行都能快速。

基本思想是盡可能避免重新翻譯。首先,探測器通過解釋器運作代碼,在執行期間,探測器會追蹤代碼段并将其會被劃分為 warm(運作少數幾次) 和 hot(運作重複多次)。

JIT 把 warm 代碼段直接丢給基準編譯器,盡可能重用已編譯的代碼。

JIT 把 hot 代碼段丢給優化編譯器,其根據解釋器收集來的資訊(1)作出假設,(2)基于假設(比如,對象屬性始終以特定順序出現)進行優化。

然而,一旦假設不成立,優化編譯器就會進行 deoptimization 去優化,就是丢棄優化的代碼。

優化和去優化的周期是昂貴的。由于需要存儲優化過的機器碼和探測器的資訊,JIT 引入了額外的記憶體成本。這種成本激發了 V8 的解釋器 Ignition 。

Ignition 将 AST 轉換為位元組碼,位元組碼序列被執行,其回報資訊被 inline caches 内聯高速緩存。 回報資訊被用于(1)Ignition 随後的解釋,和(2)TurboFan 推測性優化。

TurboFan 基于回報推測性的優化将位元組碼轉換為機器碼。

...

如何優化你的 JavaScript

1、在構造函數中聲明對象屬性

改變對象的屬性将會導緻新的隐藏類:

從 V8 優化看高效 JavaScript

本來 p1 和 p2 應該使用的是同一個隐藏類,但是由于 p1.z 的原因将會導緻它們使用不同的隐藏類,這将導緻 TurboFan 的去優化,這是應該避免的。

2、保持對象屬性排序不變

改變對象屬性的排序也将會導緻新的隐藏類:

從 V8 優化看高效 JavaScript

保持對象屬性的排序有利于重用相同的隐藏類,效率更高。

3、注意函數的參數類型

函數參數類型的更改也将會導緻去優化和重新優化:

從 V8 優化看高效 JavaScript

比如這個函數,由于參數類型的易變将會導緻編譯器無法優化。

4、在 script 域聲明類

不要在函數範圍内定義類:

從 V8 優化看高效 JavaScript

這個函數每被調用一次,一個新的原型就被會建立,每個新的原型都會對應一個新的對象 shape ,這也是無法優化的。

5、使用 for ... in

for ... in 循環是 V8 引擎特别優化過的,可以快 4 到 6 倍。

6、不相關的字元不會影響性能

早期使用的是函數的位元組計數來确定是否内聯函數,但是現在使用的是 AST 的節點數量來确定函數的大小。這就是說,諸如空格、注釋、變量名稱長度、函數簽名之類的不相關字元不會影響函數的性能。

7、Try / catch / finally 并不是毀滅性的

Try 以前會導緻昂貴的優化和去優化循環,但是現在并不會導緻明顯的性能影響。