跳至主要内容

WebAssembly 編譯流水線

WebAssembly 是一種二進位格式,可以有效率且安全地在網頁上執行非 JavaScript 的編程語言程式碼。在本文中,我們深入探討 V8 中的 WebAssembly 編譯流水線,並解釋我們如何使用不同的編譯器來提供良好的效能。

Liftoff​

最初,V8 不會編譯任何 WebAssembly 模組中的函式。相反,當函式第一次被調用時,這些函式會由基準編譯器 Liftoff 延遲編譯。Liftoff 是一種單遍編譯器,即它只需遍歷一次 WebAssembly 程式碼,並為每條 WebAssembly 指令立即生成機器碼。單遍編譯器擅長快速生成程式碼,但只能應用少量的優化措施。事實上,Liftoff 可以非常快速地編譯 WebAssembly 程式碼,每秒可編譯數十兆位元組。

一旦 Liftoff 編譯完成,生成的機器碼就會註冊到 WebAssembly 模組中,因此未來對該函式的調用可以直接使用編譯後的程式碼。

TurboFan​

Liftoff 在極短的時間內生成還算不錯的機器碼。但因為它是獨立地為每條 WebAssembly 指令生成程式碼,所以幾乎沒有空間進行優化,例如改善暫存器分配或常見編譯器優化(如冗餘載入消除、強度削減或函式內聯)。

這就是為什麼那些被頻繁執行的「熱函式」會被 TurboFan 重新編譯,TurboFan 是 V8 中的優化編譯器,適用於 WebAssembly 和 JavaScript。TurboFan 是一種多遍編譯器,這表示在生成機器碼之前,它會構建多個內部程式碼表示形式。這些額外的內部表示形式允許進一步的優化和更好的暫存器分配,從而生成顯著更快的程式碼。

V8 會監控 WebAssembly 函式的調用頻率。一旦某個函式達到某個閾值,該函式就被認為是「熱函式」,並在背景執行緒上觸發重新編譯。一旦編譯完成,生成的新程式碼將被註冊到 WebAssembly 模組中,取代現有的 Liftoff 程式碼。任何新對該函式的調用將使用由 TurboFan 生成的新優化程式碼,而不是 Liftoff 的程式碼。不過需要注意的是,我們並未進行堆疊內替換(on-stack-replacement)。也就是說,如果 TurboFan 程式碼在函式被調用後才可用,則該函式的調用將使用 Liftoff 程式碼完成其執行。

程式碼快取​

如果 WebAssembly 模組是通過 WebAssembly.compileStreaming 編譯的,那麼 TurboFan 生成的機器碼也會被快取。當同一 WebAssembly 模組再次從同一 URL 提取時,快取的程式碼可以立即使用,無需再次編譯。有關程式碼快取的更多資訊可參考另一篇部落格文章。

程式碼快取會在生成的 TurboFan 程式碼量達到某個閾值時觸發。這表示對於大型的 WebAssembly 模組,TurboFan 程式碼會逐步快取,而對於小型的 WebAssembly 模組,TurboFan 程式碼可能從未被快取。Liftoff 程式碼不會快取,因為 Liftoff 編譯幾乎與從快取中加載程式碼一樣快。

偵錯​

如前所述,TurboFan 進行了多項優化,其中許多涉及重排程式碼、消除變數或甚至跳過整個程式碼段。這意味著如果您想在特定指令處設置斷點,可能無法確定程式執行應該在哪裡停止。換句話說,TurboFan 程式碼不太適合進行偵錯。因此,當通過開啟 DevTools 開啟偵錯時,所有 TurboFan 程式碼都會再次被 Liftoff 程式碼取代(「降級」),因為每條 WebAssembly 指令精確對應於機器碼的一個段落,並且所有的局部和全域變數都保持完整。

性能分析​

為了使事情顯得更加混亂,在 DevTools 中,當 Performance 頁籤被開啟並點擊"Record"按鈕時,所有程式碼將再次被 "升級"(重新編譯為 TurboFan)。"Record" 按鈕啟動性能分析。分析 Liftoff 程式碼並不具有代表性,因為它僅在 TurboFan 尚未完成時使用,並且可能比 TurboFan 的輸出顯著更慢,而 TurboFan 的輸出將在絕大多數時間內執行。

用於試驗的標誌​

為了實驗,V8 和 Chrome 可以設定只用 Liftoff 或只用 TurboFan 編譯 WebAssembly 程式碼。甚至可以實驗延遲編譯,在函數第一次被調用時才編譯該函數。以下參數啟用這些實驗模式:

  • 僅限 Liftoff:

    • 在 V8 中,設置 --liftoff --no-wasm-tier-up 參數。
    • 在 Chrome 中,停用 WebAssembly 分層編譯(chrome://flags/#enable-webassembly-tiering),並啟用 WebAssembly 基線編譯器(chrome://flags/#enable-webassembly-baseline)。
  • 僅限 TurboFan:

    • 在 V8 中,設置 --no-liftoff --no-wasm-tier-up 參數。
    • 在 Chrome 中,停用 WebAssembly 分層編譯(chrome://flags/#enable-webassembly-tiering),並停用 WebAssembly 基線編譯器(chrome://flags/#enable-webassembly-baseline)。
  • 延遲編譯:

    • 延遲編譯是一種編譯模式,函數僅在第一次被調用時進行編譯。類似於生產配置,函數首先通過 Liftoff 編譯(阻塞執行)。在 Liftoff 編譯完成後,該函數在後臺以 TurboFan 重新編譯。
    • 在 V8 中,設置 --wasm-lazy-compilation 參數。
    • 在 Chrome 中,啟用 WebAssembly 延遲編譯(chrome://flags/#enable-webassembly-lazy-compilation)。

編譯時間​

有多種方式測量 Liftoff 和 TurboFan 的編譯時間。在 V8 的生產配置中,可以通過測量 new WebAssembly.Module() 執行完成所需的時間,或者 WebAssembly.compile() 承諾被解析所需的時間來測量 Liftoff 的編譯時間。為了測量 TurboFan 的編譯時間,可以在僅用 TurboFan 的配置中進行相同的測量。

Google Earth 的 WebAssembly 編譯的追蹤。

在 chrome://tracing/ 中啟用 v8.wasm 類別,還可以更詳細地測量編譯時間。Liftoff 編譯是從開始編譯到 wasm.BaselineFinished 事件的時間,TurboFan 編譯則在 wasm.TopTierFinished 事件結束。對於 WebAssembly.compileStreaming(),編譯從 wasm.StartStreamingCompilation 事件開始;對於 new WebAssembly.Module(),從 wasm.SyncCompile 事件開始;而對於 WebAssembly.compile(),從 wasm.AsyncCompile 事件開始。Liftoff 編譯用 wasm.BaselineCompilation 事件表示,TurboFan 編譯用 wasm.TopTierCompilation 事件表示。上圖顯示的是 Google Earth 的追蹤,並重點標註了關鍵事件。

更詳細的追蹤數據可以通過 v8.wasm.detailed 類別獲得,其中包括單個函數的編譯時間等信息。