[譯] 使 WebAssembly 更快:Firefox 的新流式和分層編譯器

人們都說 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,特別是主線程。後端

Old bottleneck, the network, on the left. New bottleneck, work on the CPU such as compiling, on the right

因此咱們想要儘量多的把工做從主線程中移除。咱們也想要儘量早的啓動它,以便咱們充分利用 CPU 的全部時間。更好的是,咱們能夠徹底減小 CPU 工做量。api

使用 JavaScript 時,你能夠作一些這樣的事情。你能夠經過流入的方式在主線程外解析文件。但你仍是須要解析它們,這就須要不少工做,而且你必須等到它們都解析完了才能開始編譯。而後編譯的時候,你又回到了主線程上。這是由於 JS 一般是運行時延遲編譯的。

Timeline showing packets coming in on the main thread, then parsing happening simultaneously on another thread. Once parse is done, execution begins on main thread, interrupted occassionally by compiling

使用 WebAssembly,啓動的工做量減小了。解碼 WebAssembly 比解析 JavaScript 更簡單,更快捷。而且這些解碼和編譯能夠跨多個線程進行拆分。

這意味着多個線程將運行基線編譯,這會讓它變得更快。一旦完成,基線編譯好的代碼就能夠在主線程上開始執行。它沒必要像 JS 代碼同樣暫停編譯。

Timeline showing packets coming in on the main thread, and decoding and baseline compiling happening across multiple threads simultaneously, resulting in execution starting faster and without compiling breaks.

當基線編譯的代碼在主線程上運行時,其餘線程則在作更優化的版本。當更優化的版本完成時,它就會替換進來使得代碼運行更加快捷。

這使得加載 WebAssembly 的成本變得更像解碼圖片而不是加載 JavaScript。而且想一想看 —— 網絡性能倡導者確定接受不了 150kB 的 JS 代碼負載量,但相同大小的圖像負載量並不會引發人們的注意。

Developer advocate on the left tsk tsk-ing about large JS file. Developer advocate on the right shrugging about large image.

這是由於圖像的加載時間要快得多,就像 Addy Osmani 在 JavaScript 的成本 中解釋的那樣,解碼圖像並不會阻塞主線程,正如 Alex Russell 在你能接受嗎?真實的 Web 性能預算中所討論的那樣。

但這並不意味着咱們但願 WebAssembly 文件和圖像文件同樣大。雖然早期的 WebAssembly 工具建立了大型的文件,是由於它們包含了不少運行時(內容),目前來看還有不少工做要作讓文件變得更小。例如,Emscripten 有一個「縮小協議」。在 Rust 中,你已經能夠經過使用 wasm32-unknown-unknown 目標來獲取至關小尺寸的文件,而且還有像 wasm-gcwasm-snip 這樣的工具來幫助進一步優化它們。

這就意味着這些 WebAssembly 文件的加載速度要比等量的 JavaScript 快得多。

這很關鍵。正如 Yehuda Katz 指出,這是一個遊戲規則改變者。

Tweet from Yehuda Katz saying it's possible to parse and compile wasm as fast as it comes over the network.

因此讓咱們看看新編譯器是怎麼工做的吧。

流式編譯:更早開始的編譯

若是你更早開始編譯代碼,你就更早完成它。這就是流式編譯所作的 —— 儘量快地開始編譯 .wasm 文件。

當你下載文件時,它不是單件式的。實際上,它帶來的是一系列數據包。

以前,當 .wasm 文件中的每一個包正在下載時,瀏覽器網絡層會把它放進 ArrayBuffer(譯者注:數組緩存)中。

Packets coming in to network layer and being added to an ArrayBuffer

而後,一旦完成下載,它會將 ArrayBuffer 轉移到 Web VM(也就是 JS 引擎)中。也就到了 WebAssembly 編譯器要開始編譯的時候。

Network layer pushing array buffer over to compiler

可是沒有充分的理由讓編譯器等待。從技術上講,逐行編譯 WebAssembly 是可行的。這意味着你可以在第一個塊進來的時候就開始啓動。

因此這就是咱們新編譯器所作的。它利用了 WebAssembly 的流式 API。

WebAssembly.instantiateStreaming call, which takes a response object with the source file. This has to be served using MIME type application/wasm.

若是你提供給 WebAssembly.instantiateStreaming 一個響應的對象,則(對象)塊一旦到達就會當即進入 WebAssembly 引擎。而後編譯器能夠開始處理第一個塊,即使下一個塊還在下載中。

Packets going directly to compiler

除了可以並行下載和編譯代碼外,它還有另一個優點。

.wasm 模塊中的代碼部分位於任何數據(它將引入到模塊的內存對象)以前。所以,經過流式傳輸,編譯器能夠在模塊的數據仍在下載的時候就對其進行編譯。若是當你的模塊須要大量的數據,且多是兆字節的時候,這些就會顯得很重要。

File split between small code section at the top, and larger data section at the bottom

經過流式傳輸,咱們能夠提早開始編譯。並且咱們一樣能夠更快速地進行編譯。

第 1 層基線編譯器:更快的編譯代碼

若是你想要代碼跑的快,你就須要優化它。可是當你編譯時執行這些優化會花費時間,也就會讓編譯代碼變得更慢。因此這裏須要一個權衡。

但魚和熊掌能夠兼得。若是咱們使用兩個編譯器,就能讓其中一個快速編譯可是不作過多的優化工做,而另外一個雖然編譯慢,可是建立了更多優化的代碼。

這就稱做爲層編譯器。當代碼第一次進入時,將由第 1 層(或基線)編譯器對其編譯。而後,當基線編譯完成,代碼開始運行以後,第 2 層編譯器再一次遍歷代碼並在後臺編譯更優化的版本。

一旦它(譯者注:第 2 層編譯)完成,它會將優化後的代碼熱插拔爲先前的基線版本。這使代碼執行得更快。

Timeline showing optimizing compiling happening in the background.

JavaScript 引擎已經使用分層編譯器很長一段時間了。然而,JS 引擎只在一些代碼變得「溫熱」 —— 當代碼的那部分被調用太屢次時,纔會使用第 2 層(或優化)編譯器。

相比之下,WebAssembly 的第 2 層編譯器會熱切地進行全面的從新編譯,優化模塊中的全部代碼。在將來,咱們可能會爲開發者添加更多選項,用來控制如何進行激進的優化或者惰性的優化。

基線編譯器在啓動時節省了大量時間。它編譯代碼的速度比優化編譯器的快 10-15 倍。而且在咱們的測試中,它建立代碼的速度只慢了 2 倍。

這意味着,只要仍在運行基線編譯代碼,即使是在最開始的幾分鐘你的代碼也會運行地很快。

並行化:讓一切更快

關於 Firefox Quantum 的文章中,我解釋了粗粒度和細粒度的並行化。咱們能夠用它們來編譯 WebAssembly。

我在上文有提到,優化編譯器會在後臺進行編譯。這意味着它空出的主線程可用於執行代碼。基線編譯版本的代碼能夠在優化編譯器進行從新編譯時運行。

但在大多數電腦上仍然會有多個核心沒有使用。爲了充分使用全部核心,兩個編譯器都使用細粒度並行化來拆解工做。

並行化的單位是功能,每一個功能均可以在不一樣的核心上單獨編譯。這就是所謂的細粒度,實際上,咱們須要將這些功能分批處理成更大的功能組。這些批次會被派送到不一樣的核內心。

...而後經過隱式緩存徹底跳過全部工做(將來的任務)

目前,每次從新加載頁面時都會重作解碼和編譯。可是若是你有相同的 .wasm 文件,它編譯後都是同樣的機器代碼。

這意味着,不少時候這些工做均可以跳過。這些也是將來咱們要作的。咱們將在第一頁加載時進行解碼和編譯,而後將生成的機器碼緩存在 HTTP 緩存中。以後當你再次請求這個 URL 的時候,它會拉取預編譯的機器代碼。

這就能讓後續加載頁面的加載時間消失了。

Timeline showing all work disappearing with caching.

這項功能已經有了基礎構建。咱們在 Firefox 58 版本中緩存了這樣的 JavaScript 字節代碼。咱們只需擴展這種支持來緩存 .wasm 文件的機器代碼。

關於 Lin Clark

Lin 是 Mozilla Developer Relations 團隊的工程師。她致力於 JavaScript、WebAssembly、Rust 和 Servo,還會繪製代碼漫畫。

Lin Clark 的更多文章...

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索