WebAssembly 系列(五)为什么 WebAssembly 更快?
作者:Lin Clark
編譯:胡子大哈
翻譯原文:huziketang.com/blog/posts/…
英文原文:What makes WebAssembly fast?
轉載請注明出處,保留原文鏈接以及作者信息
本文作者:Lin Clark
英文原文:What makes WebAssembly fast?
本文是關于 WebAssembly 系列的第五篇文章(本系列共六篇文章)。如果你沒有讀先前文章的話,建議先讀這里。如果對 WebAssembly 沒概念,建議先讀這里(中文文章)。
上一篇文章中,我介紹了如何編寫 WebAssembly 程序,也表達了我希望看到更多的開發者在自己的工程中同時使用 WebAssembly 和 JavaScript 的期許。
開發者們不必糾結于到底選擇 WebAssembly 還是 JavaScript,已經有了 JavaScript 工程的開發者們,希望能把部分 JavaScript 替換成 WebAssembly 來嘗試使用。
例如,正在開發 React 程序的團隊可以把調節器代碼(即虛擬 DOM)替換成 WebAssembly 的版本。而對于你的 web 應用的用戶來說,他們就跟以前一樣使用,不會發生任何變化,同時他們還能享受到 WebAssembly 所帶來的好處——快。
而開發者們選擇替換為 WebAssembly 的原因正是因為 WebAssembly 比較快。那么為什么它執行的快呢?我們來一起了解一下。
當前的 JavaScript 性能如何?
在我們了解 JavaScript 和 WebAssembly 的性能區別之前,需要先理解 JS 引擎的工作原理。
下面這張圖片介紹了性能使用的大概分布情況。
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 都更貼近機器碼,所以它更快,使它更快的原因有幾個:
重優化
有些情況下,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 感興趣的童鞋,歡迎指點。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的WebAssembly 系列(五)为什么 WebAssembly 更快?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简单***思路
- 下一篇: 深刻理解Python中的元类(metac