WebAssembly 編譯管線
WebAssembly 是一種二進位格式,讓你可以有效且安全地在網路上執行非 JavaScript 程式語言的程式碼。在本文中,我們深入探討 V8 中的 WebAssembly 編譯管線,並說明我們如何使用不同的編譯器來提供良好的效能。
Liftoff #
最初,V8 沒有編譯 WebAssembly 模組中的任何函式。相反地,當函式第一次被呼叫時,函式會使用基線編譯器 Liftoff 進行延遲編譯。Liftoff 是一個 單次編譯器,表示它會反覆處理 WebAssembly 程式碼一次,並立即為每個 WebAssembly 指令發出機器碼。單次編譯器擅長快速產生程式碼,但只能套用少數最佳化。的確,Liftoff 可以非常快速地編譯 WebAssembly 程式碼,每秒數十 MB。
Liftoff 編譯完成後,產生的機器碼會註冊到 WebAssembly 模組中,這樣在未來呼叫函式時,可以立即使用已編譯的程式碼。
TurboFan #
Liftoff 在很短的時間內發出相當快速的機器碼。但是,由於它獨立為每個 WebAssembly 指令發出程式碼,因此幾乎沒有最佳化的空間,例如改善暫存器配置或常見的編譯器最佳化,例如多餘載入消除、強度降低或函式內嵌。
這就是為什麼熱門函式(經常執行的函式)會使用 TurboFan 重新編譯,TurboFan 是 V8 中 WebAssembly 和 JavaScript 的最佳化編譯器。TurboFan 是一個 多重編譯器,表示它會在發出機器碼之前建立編譯程式碼的許多內部表示。這些額外的內部表示允許最佳化和更好的暫存器配置,從而產生明顯更快的程式碼。
V8 會監控 WebAssembly 函式被呼叫的頻率。一旦函式達到某個閾值,函式就會被視為熱門函式,並在背景執行緒上觸發重新編譯。編譯完成後,新程式碼會註冊到 WebAssembly 模組中,取代現有的 Liftoff 程式碼。對該函式的任何新呼叫都將使用 TurboFan 產生的新的最佳化程式碼,而不是 Liftoff 程式碼。不過請注意,我們不進行堆疊上替換。這表示如果在函式被呼叫後 TurboFan 程式碼才可用,函式呼叫將使用 Liftoff 程式碼完成執行。
程式碼快取 #
如果 WebAssembly 模組是用 WebAssembly.compileStreaming
編譯,則 TurboFan 產生的機器碼也會快取。當從同一個 URL 再次擷取相同的 WebAssembly 模組時,快取的程式碼就可以立即使用,而不需要額外編譯。有關程式碼快取的更多資訊,請參閱 另一篇網誌文章。
當產生的 TurboFan 程式碼量達到某個臨界值時,就會觸發程式碼快取。這表示對於大型 WebAssembly 模組,TurboFan 程式碼會以遞增方式快取,而對於小型 WebAssembly 模組,TurboFan 程式碼可能永遠不會快取。Liftoff 程式碼不會快取,因為 Liftoff 編譯幾乎和從快取載入程式碼一樣快。
偵錯 #
如前所述,TurboFan 會套用最佳化,其中許多最佳化都涉及重新排序程式碼、移除變數,甚至跳過整段程式碼。這表示如果您要在特定指令中設定中斷點,可能不清楚程式執行實際上應該在哪裡停止。換句話說,TurboFan 程式碼不適合偵錯。因此,當透過開啟 DevTools 開始偵錯時,所有 TurboFan 程式碼都會再次以 Liftoff 程式碼取代(「降階」),因為每個 WebAssembly 指令都對應到機器碼的一個區段,而且所有區域和全域變數都完好無損。
剖析 #
為了讓事情更令人困惑,在 DevTools 中,當開啟「效能」標籤並按一下「記錄」按鈕時,所有程式碼都會再次升階(使用 TurboFan 重新編譯)。「記錄」按鈕會開始效能剖析。剖析 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
)。
- 在 V8 中,設定
僅 TurboFan
- 在 V8 中,設定
--no-liftoff --no-wasm-tier-up
旗標。 - 在 Chrome 中,停用 WebAssembly 分層 (
chrome://flags/#enable-webassembly-tiering
) 和停用 WebAssembly 基準編譯器 (chrome://flags/#enable-webassembly-baseline
)。
- 在 V8 中,設定
延遲編譯
- 延遲編譯是一種編譯模式,其中函式僅在首次呼叫時編譯。與生產組態類似,函式會先使用 Liftoff 編譯 (封鎖執行)。在 Liftoff 編譯完成後,函式會在背景中使用 TurboFan 重新編譯。
- 在 V8 中,設定
--wasm-lazy-compilation
旗標。 - 在 Chrome 中,啟用 WebAssembly 延遲編譯 (
chrome://flags/#enable-webassembly-lazy-compilation
)。
編譯時間 #
有不同的方式可以衡量 Liftoff 和 TurboFan 的編譯時間。在 V8 的生產組態中,Liftoff 的編譯時間可以透過 JavaScript 衡量,方法是衡量 new WebAssembly.Module()
完成所需的時間,或 WebAssembly.compile()
解決承諾所需的時間。若要衡量 TurboFan 的編譯時間,可以在僅 TurboFan 的組態中執行相同的動作。
編譯也可以在 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
類別取得,其中除了其他資訊外,還提供了單一函式的編譯時間。