- 原文地址:Making WebAssembly even faster: Firefox’s new streaming and tiering compiler
- 原文做者:Lin Clark
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Sam
- 校對者:Augustwuli
人們都說 WebAssembly 是一個遊戲規則改變者,由於它可讓代碼更快地在網絡上運行。有些加速已經存在,還有些在不遠的未來。javascript
其中一種加速是流式編譯,即瀏覽器在代碼還在下載的時候就對其進行編譯。截至目前,這只是潛在的將來加速(方式)。但隨着下週 Firefox 58 版本的發佈,它將成爲現實。前端
Firefox 58 還包含兩層新的編譯器。新的基線編譯器編譯代碼的速度比優化編譯器快了 10-15 倍。java
綜合起來,這兩個變化意味着咱們編譯代碼的速度比從網絡中編譯代碼速度快。android
在臺式電腦上,咱們每秒編譯 30-60 兆字節的 WebAssembly 代碼。這比網絡傳送數據包的速度還快。ios
若是你使用 Firefox Nightly 或者 Beta,你能夠在你本身設備上試一試。即使是在很普通的移動設備上,咱們能夠每秒編譯 8 兆字節 —— 這比任何移動網絡的平均下載速度都要快得多。git
這意味着你的代碼幾乎是在它完成下載後就當即執行。github
當網站發佈大批量 JavaScript 代碼時,Web 性能擁護者會變得一籌莫展。這是由於下載大量的 JavaScript 會讓頁面加載變慢。web
這很大程度是由於解析和編譯時間。正如 Steve Souder 指出,網絡性能的舊瓶頸曾是網絡。但如今網絡性能的新瓶頸是 CPU,特別是主線程。後端
因此咱們想要儘量多的把工做從主線程中移除。咱們也想要儘量早的啓動它,以便咱們充分利用 CPU 的全部時間。更好的是,咱們能夠徹底減小 CPU 工做量。api
使用 JavaScript 時,你能夠作一些這樣的事情。你能夠經過流入的方式在主線程外解析文件。但你仍是須要解析它們,這就須要不少工做,而且你必須等到它們都解析完了才能開始編譯。而後編譯的時候,你又回到了主線程上。這是由於 JS 一般是運行時延遲編譯的。
使用 WebAssembly,啓動的工做量減小了。解碼 WebAssembly 比解析 JavaScript 更簡單,更快捷。而且這些解碼和編譯能夠跨多個線程進行拆分。
這意味着多個線程將運行基線編譯,這會讓它變得更快。一旦完成,基線編譯好的代碼就能夠在主線程上開始執行。它沒必要像 JS 代碼同樣暫停編譯。
當基線編譯的代碼在主線程上運行時,其餘線程則在作更優化的版本。當更優化的版本完成時,它就會替換進來使得代碼運行更加快捷。
這使得加載 WebAssembly 的成本變得更像解碼圖片而不是加載 JavaScript。而且想一想看 —— 網絡性能倡導者確定接受不了 150kB 的 JS 代碼負載量,但相同大小的圖像負載量並不會引發人們的注意。
這是由於圖像的加載時間要快得多,就像 Addy Osmani 在 JavaScript 的成本 中解釋的那樣,解碼圖像並不會阻塞主線程,正如 Alex Russell 在你能接受嗎?真實的 Web 性能預算中所討論的那樣。
但這並不意味着咱們但願 WebAssembly 文件和圖像文件同樣大。雖然早期的 WebAssembly 工具建立了大型的文件,是由於它們包含了不少運行時(內容),目前來看還有不少工做要作讓文件變得更小。例如,Emscripten 有一個「縮小協議」。在 Rust 中,你已經能夠經過使用 wasm32-unknown-unknown 目標來獲取至關小尺寸的文件,而且還有像 wasm-gc 和 wasm-snip 這樣的工具來幫助進一步優化它們。
這就意味着這些 WebAssembly 文件的加載速度要比等量的 JavaScript 快得多。
這很關鍵。正如 Yehuda Katz 指出,這是一個遊戲規則改變者。
因此讓咱們看看新編譯器是怎麼工做的吧。
若是你更早開始編譯代碼,你就更早完成它。這就是流式編譯所作的 —— 儘量快地開始編譯 .wasm 文件。
當你下載文件時,它不是單件式的。實際上,它帶來的是一系列數據包。
以前,當 .wasm 文件中的每一個包正在下載時,瀏覽器網絡層會把它放進 ArrayBuffer(譯者注:數組緩存)中。
而後,一旦完成下載,它會將 ArrayBuffer 轉移到 Web VM(也就是 JS 引擎)中。也就到了 WebAssembly 編譯器要開始編譯的時候。
可是沒有充分的理由讓編譯器等待。從技術上講,逐行編譯 WebAssembly 是可行的。這意味着你可以在第一個塊進來的時候就開始啓動。
因此這就是咱們新編譯器所作的。它利用了 WebAssembly 的流式 API。
若是你提供給 WebAssembly.instantiateStreaming
一個響應的對象,則(對象)塊一旦到達就會當即進入 WebAssembly 引擎。而後編譯器能夠開始處理第一個塊,即使下一個塊還在下載中。
除了可以並行下載和編譯代碼外,它還有另一個優點。
.wasm 模塊中的代碼部分位於任何數據(它將引入到模塊的內存對象)以前。所以,經過流式傳輸,編譯器能夠在模塊的數據仍在下載的時候就對其進行編譯。若是當你的模塊須要大量的數據,且多是兆字節的時候,這些就會顯得很重要。
經過流式傳輸,咱們能夠提早開始編譯。並且咱們一樣能夠更快速地進行編譯。
若是你想要代碼跑的快,你就須要優化它。可是當你編譯時執行這些優化會花費時間,也就會讓編譯代碼變得更慢。因此這裏須要一個權衡。
但魚和熊掌能夠兼得。若是咱們使用兩個編譯器,就能讓其中一個快速編譯可是不作過多的優化工做,而另外一個雖然編譯慢,可是建立了更多優化的代碼。
這就稱做爲層編譯器。當代碼第一次進入時,將由第 1 層(或基線)編譯器對其編譯。而後,當基線編譯完成,代碼開始運行以後,第 2 層編譯器再一次遍歷代碼並在後臺編譯更優化的版本。
一旦它(譯者注:第 2 層編譯)完成,它會將優化後的代碼熱插拔爲先前的基線版本。這使代碼執行得更快。
JavaScript 引擎已經使用分層編譯器很長一段時間了。然而,JS 引擎只在一些代碼變得「溫熱」 —— 當代碼的那部分被調用太屢次時,纔會使用第 2 層(或優化)編譯器。
相比之下,WebAssembly 的第 2 層編譯器會熱切地進行全面的從新編譯,優化模塊中的全部代碼。在將來,咱們可能會爲開發者添加更多選項,用來控制如何進行激進的優化或者惰性的優化。
基線編譯器在啓動時節省了大量時間。它編譯代碼的速度比優化編譯器的快 10-15 倍。而且在咱們的測試中,它建立代碼的速度只慢了 2 倍。
這意味着,只要仍在運行基線編譯代碼,即使是在最開始的幾分鐘你的代碼也會運行地很快。
在關於 Firefox Quantum 的文章中,我解釋了粗粒度和細粒度的並行化。咱們能夠用它們來編譯 WebAssembly。
我在上文有提到,優化編譯器會在後臺進行編譯。這意味着它空出的主線程可用於執行代碼。基線編譯版本的代碼能夠在優化編譯器進行從新編譯時運行。
但在大多數電腦上仍然會有多個核心沒有使用。爲了充分使用全部核心,兩個編譯器都使用細粒度並行化來拆解工做。
並行化的單位是功能,每一個功能均可以在不一樣的核心上單獨編譯。這就是所謂的細粒度,實際上,咱們須要將這些功能分批處理成更大的功能組。這些批次會被派送到不一樣的核內心。
目前,每次從新加載頁面時都會重作解碼和編譯。可是若是你有相同的 .wasm 文件,它編譯後都是同樣的機器代碼。
這意味着,不少時候這些工做均可以跳過。這些也是將來咱們要作的。咱們將在第一頁加載時進行解碼和編譯,而後將生成的機器碼緩存在 HTTP 緩存中。以後當你再次請求這個 URL 的時候,它會拉取預編譯的機器代碼。
這就能讓後續加載頁面的加載時間消失了。
這項功能已經有了基礎構建。咱們在 Firefox 58 版本中緩存了這樣的 JavaScript 字節代碼。咱們只需擴展這種支持來緩存 .wasm 文件的機器代碼。
Lin 是 Mozilla Developer Relations 團隊的工程師。她致力於 JavaScript、WebAssembly、Rust 和 Servo,還會繪製代碼漫畫。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。