文本翻譯自: 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、在構造函數中聲明對象屬性
改變對象的屬性将會導緻新的隐藏類:
本來 p1 和 p2 應該使用的是同一個隐藏類,但是由于 p1.z 的原因将會導緻它們使用不同的隐藏類,這将導緻 TurboFan 的去優化,這是應該避免的。
2、保持對象屬性排序不變
改變對象屬性的排序也将會導緻新的隐藏類:
保持對象屬性的排序有利于重用相同的隐藏類,效率更高。
3、注意函數的參數類型
函數參數類型的更改也将會導緻去優化和重新優化:
比如這個函數,由于參數類型的易變将會導緻編譯器無法優化。
4、在 script 域聲明類
不要在函數範圍内定義類:
這個函數每被調用一次,一個新的原型就被會建立,每個新的原型都會對應一個新的對象 shape ,這也是無法優化的。
5、使用 for ... in
for ... in 循環是 V8 引擎特别優化過的,可以快 4 到 6 倍。
6、不相關的字元不會影響性能
早期使用的是函數的位元組計數來确定是否内聯函數,但是現在使用的是 AST 的節點數量來确定函數的大小。這就是說,諸如空格、注釋、變量名稱長度、函數簽名之類的不相關字元不會影響函數的性能。
7、Try / catch / finally 并不是毀滅性的
Try 以前會導緻昂貴的優化和去優化循環,但是現在并不會導緻明顯的性能影響。