title: [A crash course in WebAssembly] 為什麼WebAssembly這麼快
date: 2018-3-22 23:58:00
categories: 翻譯
tags: WebAssembly
source: 原文地址
auther: Lin Clark
[A crash course to WebAssembly] 為什麼WebAssembly這麼快
這是WebAssembly系列文章的第五部分,說明了它的快速之處。如果您還沒有閱讀其他文章,我們建議您從頭開始。
在上一篇文章中,我解釋說使用WebAssembly或JavaScript進行程式設計不是一種是或不是的選擇。我們並不期望太多的開發人員會編寫完整的WebAssembly程式碼庫。
所以開發人員不需要在他們的應用程式中選擇使用WebAssembly還是JavaScript。但是,我們希望開發人員將Web部件的JavaScript程式碼部分換掉。
例如,使用React的團隊可以將他們的調解器程式碼(reconciler code,又名虛擬DOM)用WebAssembly版本替換,而使用React的人不需要做任何事情......他們的應用程式不但可以像以前一樣工作,還能夠獲得使用WebAssembly的好處。
開發人員之所以喜歡React團隊的開發人員進行這種替換,是因為WebAssembly速度更快。但關鍵是什麼讓它更快?
今天的JavaScript效能如何?
在我們理解JavaScript和WebAssembly之間的效能差異之前,我們需要了解JS引擎所做的工作。
這張圖給出了一個應用程式啟動效能的粗略情況。
JS引擎執行這些任務的時間取決於頁面使用的JavaScript。此圖並不意味著代表精確的效能數字。相反,它旨在提供一個高階模型,用於說明JS和WebAssembly中相同函式的效能會有所不同。
每個格子顯示花在完成特定任務上的時間。
-
Parsing - 解析 - 將原始碼處理成直譯器可以執行的東西所需的時間。
-
Compiling + optimizing - 編譯 + 優化 - 在基線編譯器和優化編譯器中花費的時間。一些優化編譯器的工作不在主執行緒中,所以不包含在這裡。
-
Re-optimizing - 重優化(去優化 + 優化) - 當JIT的*假設(assumptions)*失敗時,JIT重新優化程式碼和將優化後的程式碼回退到基線程式碼(baseline code)花費的格外的時間。
-
Execution - 執行 - 執行程式碼所需的時間。
-
Garbage collection - 垃圾回收 — 畫在記憶體清理上的時間。
需要注意的一件重要事情是:這些任務不會以離散塊或特定順序發生。相反,它們是交錯的。一些解析將會發生,然後是一些執行,然後是一些編譯,然後是一些更多的解析,然後是更多的執行等等。
這種改進所帶來的效能是JavaScript早期的一大改進,從前的效能圖看起來更像這樣:
一開始,當它只是一個執行JavaScript的直譯器時,執行速度很慢。當JIT被引入時,它大大加快了執行時間。
JIT權衡了監視和編譯程式碼的開銷。如果JavaScript開發人員一直以相同的方式編寫JavaScript,那麼解析和編譯時間將會大幅減少。但效能的提升誘使開發人員建立更大的JavaScript應用程式。
這也意味著還有改進的餘地。
WebAssembly的優勢又在哪裡?
下面是WebAssembly與典型Web應用程式的效能比較示意圖。
不同瀏覽器之間在處理這些階段方面各有實現。我在這裡使用SpiderMonkey作為我的模型。
Fetching
這在示意圖中沒有標示,但確實需要花費時間的一件事就是從伺服器上獲取檔案。
因為WebAssembly比JavaScript更緊湊,所以抓取速度更快。儘管壓縮演算法可以顯著減小JavaScript包的大小,但WebAssembly的壓縮的二進位制表示仍然具有優勢。
這意味著它在伺服器和客戶端之間傳輸所花費的時間更少。這在慢速網路上尤其明顯。
Parsing
一旦它到達瀏覽器,JavaScript原始碼就會被解析為抽象語法樹(AST)。
瀏覽器通常會惰性地執行此操作,只先解析它們真正需要的內容,然後為尚未呼叫的函式建立存根(stubs)。
在這個階段,AST被轉換為特定於該JS引擎的中間表示(intermediate representation - IR,為 bytecode - 位元組碼)
相反,WebAssembly不需要經過這個轉換,因為它已經是一個IR。它只需要解碼和驗證,以確保其中沒有錯誤。
Compiling + optimizing
正如我在關於JIT的文章中解釋的那樣,JavaScript是在執行程式碼期間編譯的。根據執行時(Runtime)使用的型別,可能需要編譯相同程式碼的多個版本。
不同的瀏覽器處理編譯WebAssembly的方式不同。一些瀏覽器在開始執行WebAssembly之前進行基線編譯,一些則使用JIT。
無論哪種方式,WebAssembly從一開始就更接近機器碼。例如: 型別是程式的一部分。以下是幾個快速的原因:
-
編譯器不必花費時間執行程式碼,以在開始編譯優化的程式碼之前觀察正在使用的型別。
-
編譯器不必根據它觀察到的不同型別編譯相同程式碼的不同版本。
-
在LLVM中已經提前做了很多的優化。因此編譯和優化工作花費的時間更少。
Reoptimizing
有時JIT必須丟棄程式碼的優化版本並再次嘗試優化。
當JIT基於已執行程式碼做出的假設不正確時,就會發生這種情況。例如,當進入迴圈的變數與先前迭代中的變數不同時,或者在原型鏈中插入新函式時,就會發生去優化。
去優化有兩個成本。首先,退出優化後的程式碼並返回到基準版本需要一段時間。其次,如果該函式仍被大量呼叫,則JIT可能決定再次向優化編譯器傳送它,重複優化。
在WebAssembly中,像型別這樣的東西是顯式的,所以JIT不需要根據它在執行時收集的資料來對型別進行假設。這意味著重優化將被省略。
Executing
要編寫能夠如預期執行的JavaScript程式碼,你需要了解JIT所做的優化。例如,你需要知道怎麼樣編寫程式碼,才便於編譯器優化程式碼,這在JIT文章中有所描述。
但是大多數開發人員並不瞭解JIT內部。 即使對那些瞭解JIT內部的開發人員來說,也很難達到最佳狀態。 人們用來使程式碼更易讀的許多編碼模式(例如將通用任務抽象為跨型別函式的函式,範型)妨礙了編譯器優化程式碼。
另外,不同瀏覽器之間JIT使用的優化是不同的,所以同樣的編碼可能在另一個瀏覽器中表現出更低的效能。
因此,在WebAssembly中程式碼的執行通常更快。 WebAssembly並不需要JIT對JavaScript進行的許多優化(例如型別專用化)。
另外,WebAssembly被設計為編譯導向的。 這意味著它被設計為由編譯器生成,而不是由程式設計師手工編寫。
由於人類程式設計師不需要直接對其進行程式設計,因此WebAssembly可以提供一系列對機器更為理想的指令。根據程式碼的工作型別,這些指令的執行增速從10%到800%不等。
Garbage collection
在JavaScript中,開發人員無需擔心變數的回收處理,JS引擎通過使用稱為垃圾回收器的東西自動執行該操作。
v8 wiki 中有提到GC的工作機制————整個世界都會為之停止
但是,如果你想要可預測的效能,這就可能會成為問題。你無法控制垃圾回收器何時工作,它可能會在不大方便的時候發生。大多數瀏覽器已經很好地排程它,但這依舊會阻礙程式碼的執行。
WebAssembly不支援垃圾收集(至少現在)。記憶體是手動管理的(就像 C/C++ & Rust 這樣的語言)。雖然這可能會使開發人員的程式設計變得更加困難,但它也可以使效能更加一致。
總結
在許多情況下,WebAssembly比JavaScript快,因為:
-
獲取WebAssembly所需的時間較少,壓縮率相比JS也更高。
-
解碼WebAssembly比解析JavaScript花費更少的時間。
-
編譯和優化花費更少的時間,因為WebAssembly比JavaScript更接近機器碼,並且已經在伺服器端進行了優化。
-
重新優化不會發生,因為WebAssembly內建了型別和其他資訊,所以JS引擎不需要像優化JavaScript時那樣推測它何時可以優化。
-
執行通常花費的時間更少,因為減少了不同瀏覽器的優化差異,而且WebAssembly的指令集對於機器來說更為理想。
-
不需要GC。
這就是為什麼在許多情況下,WebAssembly在執行相同任務時效能將大大超越JavaScript。
在某些情況下,WebAssembly的表現並不像預期的那樣好,但同樣也在某些場景中獲得更高的效能。我將在下一篇文章中介紹這些內容。