WebAssembly 系列(五)爲何 WebAssembly 更快?

做者:Lin Clark

編譯:鬍子大哈 javascript

翻譯原文:huziketang.com/blog/posts/…

英文原文:What makes WebAssembly fast?java

轉載請註明出處,保留原文連接以及做者信息react


本文做者:Lin Clark
英文原文:What makes WebAssembly fast?web

本文是關於 WebAssembly 系列的第五篇文章(本系列共六篇文章)。若是你沒有讀先前文章的話,建議先讀這裏。若是對 WebAssembly 沒概念,建議先讀這裏(中文文章)算法

上一篇文章中,我介紹瞭如何編寫 WebAssembly 程序,也表達了我但願看到更多的開發者在本身的工程中同時使用 WebAssembly 和 JavaScript 的期許。編程

開發者們沒必要糾結於到底選擇 WebAssembly 仍是 JavaScript,已經有了 JavaScript 工程的開發者們,但願能把部分 JavaScript 替換成 WebAssembly 來嘗試使用。瀏覽器

例如,正在開發 React 程序的團隊能夠把調節器代碼(即虛擬 DOM)替換成 WebAssembly 的版本。而對於你的 web 應用的用戶來講,他們就跟之前同樣使用,不會發生任何變化,同時他們還能享受到 WebAssembly 所帶來的好處——快。服務器

而開發者們選擇替換爲 WebAssembly 的緣由正是由於 WebAssembly 比較快。那麼爲何它執行的快呢?咱們來一塊兒瞭解一下。網絡

當前的 JavaScript 性能如何?

在咱們瞭解 JavaScript 和 WebAssembly 的性能區別以前,須要先理解 JS 引擎的工做原理。ide

下面這張圖片介紹了性能使用的大概分佈狀況。

JS 引擎在圖中各個部分所花的時間取決於頁面所用的 JavaScript 代碼。圖表中的比例並不表明真實狀況下的確切比例狀況。

圖中的每個顏色條都表明了不一樣的任務:

  • Parsing——表示把源代碼變成解釋器能夠運行的代碼所花的時間;
  • Compiling + optimizing——表示基線編譯器和優化編譯器花的時間。一些優化編譯器的工做並不在主線程運行,不包含在這裏。
  • Re-optimizing——當 JIT 發現優化假設錯誤,丟棄優化代碼所花的時間。包括重優化的時間、拋棄並返回到基線編譯器的時間。
  • Execution——執行代碼的時間
  • Garbage collection——垃圾回收,清理內存的時間

這裏注意:這些任務並非離散執行的,或者按固定順序依次執行的。而是交叉執行,好比正在進行解析過程時,其餘一些代碼正在運行,而另外一些正在編譯。

這樣的交叉執行給早期 JavaScript 帶來了很大的效率提高,早期的 JavaScript 執行相似於下圖,各個過程順序進行:

早期時,JavaScript 只有解釋器,執行起來很是慢。當引入了 JIT 後,大大提高了執行效率,縮短了執行時間。

JIT 所付出的開銷是對代碼的監視和編譯時間。JavaScript 開發者能夠像之前那樣開發 JavaScript 程序,而一樣的程序,解析和編譯的時間也大大縮短。這就使得開發者們更加傾向於開發更復雜的 JavaScript 應用。

同時,這也說明了執行效率上還有很大的提高空間。

WebAssembly 對比

下面是 WebAssembly 和典型的 web 應用的近似對比圖:

各類瀏覽器處理上圖中不一樣的過程,有着細微的差異,我用 SpiderMonkey 做爲模型來說解不一樣的階段:

文件獲取

這一步並無顯示在圖表中,可是這看似簡單地從服務器獲取文件這個步驟,卻會花費很長時間。

WebAssembly 比 JavaScript 的壓縮率更高,因此文件獲取也更快。即使經過壓縮算法能夠顯著地減少 JavaScript 的包大小,可是壓縮後的 WebAssembly 的二進制代碼依然更小。

這就是說在服務器和客戶端之間傳輸文件更快,尤爲在網絡很差的狀況下。

解析

當到達瀏覽器時,JavaScript 源代碼就被解析成了抽象語法樹。

瀏覽器採用懶加載的方式進行,只解析真正須要的部分,而對於瀏覽器暫時不須要的函數只保留它的樁。

解析事後 AST (抽象語法樹)就變成了中間代碼(叫作字節碼),提供給 JS 引擎編譯。

而 WebAssembly 則不須要這種轉換,由於它自己就是中間代碼。它要作的只是解碼而且檢查確認代碼沒有錯誤就能夠了。

編譯和優化

上一篇關於 JIT 的文章中,我有介紹過,JavaScript 是在代碼的執行階段編譯的。由於它是弱類型語言,當變量類型發生變化時,一樣的代碼會被編譯成不一樣版本。

不一樣瀏覽器處理 WebAssembly 的編譯過程也不一樣,有些瀏覽器只對 WebAssembly 作基線編譯,而另外一些瀏覽器用 JIT 來編譯。

不論哪一種方式,WebAssembly 都更貼近機器碼,因此它更快,使它更快的緣由有幾個:

  1. 在編譯優化代碼以前,它不須要提早運行代碼以知道變量都是什麼類型。
  2. 編譯器不須要對一樣的代碼作不一樣版本的編譯。
  3. 不少優化在 LLVM 階段就已經作完了,因此在編譯和優化的時候沒有太多的優化須要作。

重優化

有些狀況下,JIT 會反覆地進行「拋棄優化代碼<->重優化」過程。

當 JIT 在優化假設階段作的假設,執行階段發現是不正確的時候,就會發生這種狀況。好比當循環中發現本次循環所使用的變量類型和上次循環的類型不同,或者原型鏈中插入了新的函數,都會使 JIT 拋棄已優化的代碼。

反優化過程有兩部分開銷。第一,須要花時間丟掉已優化的代碼而且回到基線版本。第二,若是函數依舊頻繁被調用,JIT 可能會再次把它發送到優化編譯器,又作一次優化編譯,這是在作無用功。

在 WebAssembly 中,類型都是肯定了的,因此 JIT 不須要根據變量的類型作優化假設。也就是說 WebAssembly 沒有重優化階段。

執行

本身也能夠寫出執行效率很高的 JavaScript 代碼。你須要瞭解 JIT 的優化機制,例如你要知道什麼樣的代碼編譯器會對其進行特殊處理(JIT 文章裏面有提到過)。

然而大多數的開發者是不知道 JIT 內部的實現機制的。即便開發者知道 JIT 的內部機制,也很難寫出符合 JIT 標準的代碼,由於人們一般爲了代碼可讀性更好而使用的編碼模式,偏偏不合適編譯器對代碼的優化。

加之 JIT 會針對不一樣的瀏覽器作不一樣的優化,因此對於一個瀏覽器優化的比較好,極可能在另一個瀏覽器上執行效率就比較差。

正是由於這樣,執行 WebAssembly 一般會比較快,不少 JIT 爲 JavaScript 所作的優化在 WebAssembly 並不須要。另外,WebAssembly 就是爲了編譯器而設計的,開發人員不直接對其進行編程,這樣就使得 WebAssembly 專一於提供更加理想的指令(執行效率更高的指令)給機器就行了。

執行效率方面,不一樣的代碼功能有不一樣的效果,通常來說執行效率會提升 10% - 800%。

垃圾回收

JavaScript 中,開發者不須要手動清理內存中不用的變量。JS 引擎會自動地作這件事情,這個過程叫作垃圾回收。

但是,當你想要實現性能可控,垃圾回收可能就是個問題了。垃圾回收器會自動開始,這是不受你控制的,因此頗有可能它會在一個不合適的時機啓動。目前的大多數瀏覽器已經能給垃圾回收安排一個合理的啓動時間,不過這仍是會增長代碼執行的開銷。

目前爲止,WebAssembly 不支持垃圾回收。內存操做都是手動控制的(像 C、C++同樣)。這對於開發者來說確實增長了些開發成本,不過這也使代碼的執行效率更高。

總結

WebAssembly 比 JavaScript 執行更快是由於:

  • 文件抓取階段,WebAssembly 比 JavaScript 抓取文件更快。即便 JavaScript 進行了壓縮,WebAssembly 文件的體積也比 JavaScript 更小;
  • 解析階段,WebAssembly 的解碼時間比 JavaScript 的解析時間更短;
  • 編譯和優化階段,WebAssembly 更具優點,由於 WebAssembly 的代碼更接近機器碼,而 JavaScript 要先經過服務器端進行代碼優化。
  • 重優化階段,WebAssembly 不會發生重優化現象。而 JS 引擎的優化假設則可能會發生「拋棄優化代碼<->重優化」現象。
  • 執行階段,WebAssembly 更快是由於開發人員不須要懂太多的編譯器技巧,而這在 JavaScript 中是須要的。WebAssembly 代碼也更適合生成機器執行效率更高的指令。
  • 垃圾回收階段,WebAssembly 垃圾回收都是手動控制的,效率比自動回收更高。

這就是爲何在大多數狀況下,同一個任務 WebAssembly 比 JavaScript 表現更好的緣由。

可是,還有一些狀況 WebAssembly 表現的會不如預期;同時 WebAssembly 的將來也會朝着使 WebAssembly 執行效率更高的方向發展。這些我會在下一篇文章《WebAssembly 系列(六)WebAssembly 的如今與將來》中介紹。


我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點

相關文章
相關標籤/搜索