WebAssembly 系列(五)为什么 WebAssembly 更快?
作者:Lin Clark
編譯:胡子大哈
翻譯原文:huziketang.com/blog/posts/…
英文原文:What makes WebAssembly fast?
轉(zhuǎn)載請注明出處,保留原文鏈接以及作者信息
本文作者:Lin Clark
英文原文:What makes WebAssembly fast?
本文是關(guān)于 WebAssembly 系列的第五篇文章(本系列共六篇文章)。如果你沒有讀先前文章的話,建議先讀這里。如果對 WebAssembly 沒概念,建議先讀這里(中文文章)。
上一篇文章中,我介紹了如何編寫 WebAssembly 程序,也表達(dá)了我希望看到更多的開發(fā)者在自己的工程中同時(shí)使用 WebAssembly 和 JavaScript 的期許。
開發(fā)者們不必糾結(jié)于到底選擇 WebAssembly 還是 JavaScript,已經(jīng)有了 JavaScript 工程的開發(fā)者們,希望能把部分 JavaScript 替換成 WebAssembly 來嘗試使用。
例如,正在開發(fā) React 程序的團(tuán)隊(duì)可以把調(diào)節(jié)器代碼(即虛擬 DOM)替換成 WebAssembly 的版本。而對于你的 web 應(yīng)用的用戶來說,他們就跟以前一樣使用,不會發(fā)生任何變化,同時(shí)他們還能享受到 WebAssembly 所帶來的好處——快。
而開發(fā)者們選擇替換為 WebAssembly 的原因正是因?yàn)?WebAssembly 比較快。那么為什么它執(zhí)行的快呢?我們來一起了解一下。
當(dāng)前的 JavaScript 性能如何?
在我們了解 JavaScript 和 WebAssembly 的性能區(qū)別之前,需要先理解 JS 引擎的工作原理。
下面這張圖片介紹了性能使用的大概分布情況。
JS 引擎在圖中各個(gè)部分所花的時(shí)間取決于頁面所用的 JavaScript 代碼。圖表中的比例并不代表真實(shí)情況下的確切比例情況。
圖中的每一個(gè)顏色條都代表了不同的任務(wù):
- Parsing——表示把源代碼變成解釋器可以運(yùn)行的代碼所花的時(shí)間;
- Compiling + optimizing——表示基線編譯器和優(yōu)化編譯器花的時(shí)間。一些優(yōu)化編譯器的工作并不在主線程運(yùn)行,不包含在這里。
- Re-optimizing——當(dāng) JIT 發(fā)現(xiàn)優(yōu)化假設(shè)錯(cuò)誤,丟棄優(yōu)化代碼所花的時(shí)間。包括重優(yōu)化的時(shí)間、拋棄并返回到基線編譯器的時(shí)間。
- Execution——執(zhí)行代碼的時(shí)間
- Garbage collection——垃圾回收,清理內(nèi)存的時(shí)間
這里注意:這些任務(wù)并不是離散執(zhí)行的,或者按固定順序依次執(zhí)行的。而是交叉執(zhí)行,比如正在進(jìn)行解析過程時(shí),其他一些代碼正在運(yùn)行,而另一些正在編譯。
這樣的交叉執(zhí)行給早期 JavaScript 帶來了很大的效率提升,早期的 JavaScript 執(zhí)行類似于下圖,各個(gè)過程順序進(jìn)行:
早期時(shí),JavaScript 只有解釋器,執(zhí)行起來非常慢。當(dāng)引入了 JIT 后,大大提升了執(zhí)行效率,縮短了執(zhí)行時(shí)間。
JIT 所付出的開銷是對代碼的監(jiān)視和編譯時(shí)間。JavaScript 開發(fā)者可以像以前那樣開發(fā) JavaScript 程序,而同樣的程序,解析和編譯的時(shí)間也大大縮短。這就使得開發(fā)者們更加傾向于開發(fā)更復(fù)雜的 JavaScript 應(yīng)用。
同時(shí),這也說明了執(zhí)行效率上還有很大的提升空間。
WebAssembly 對比
下面是 WebAssembly 和典型的 web 應(yīng)用的近似對比圖:
各種瀏覽器處理上圖中不同的過程,有著細(xì)微的差別,我用 SpiderMonkey 作為模型來講解不同的階段:
文件獲取
這一步并沒有顯示在圖表中,但是這看似簡單地從服務(wù)器獲取文件這個(gè)步驟,卻會花費(fèi)很長時(shí)間。
WebAssembly 比 JavaScript 的壓縮率更高,所以文件獲取也更快。即便通過壓縮算法可以顯著地減小 JavaScript 的包大小,但是壓縮后的 WebAssembly 的二進(jìn)制代碼依然更小。
這就是說在服務(wù)器和客戶端之間傳輸文件更快,尤其在網(wǎng)絡(luò)不好的情況下。
解析
當(dāng)?shù)竭_(dá)瀏覽器時(shí),JavaScript 源代碼就被解析成了抽象語法樹。
瀏覽器采用懶加載的方式進(jìn)行,只解析真正需要的部分,而對于瀏覽器暫時(shí)不需要的函數(shù)只保留它的樁。
解析過后 AST (抽象語法樹)就變成了中間代碼(叫做字節(jié)碼),提供給 JS 引擎編譯。
而 WebAssembly 則不需要這種轉(zhuǎn)換,因?yàn)樗旧砭褪侵虚g代碼。它要做的只是解碼并且檢查確認(rèn)代碼沒有錯(cuò)誤就可以了。
編譯和優(yōu)化
上一篇關(guān)于 JIT 的文章中,我有介紹過,JavaScript 是在代碼的執(zhí)行階段編譯的。因?yàn)樗侨躅愋驼Z言,當(dāng)變量類型發(fā)生變化時(shí),同樣的代碼會被編譯成不同版本。
不同瀏覽器處理 WebAssembly 的編譯過程也不同,有些瀏覽器只對 WebAssembly 做基線編譯,而另一些瀏覽器用 JIT 來編譯。
不論哪種方式,WebAssembly 都更貼近機(jī)器碼,所以它更快,使它更快的原因有幾個(gè):
重優(yōu)化
有些情況下,JIT 會反復(fù)地進(jìn)行“拋棄優(yōu)化代碼<->重優(yōu)化”過程。
當(dāng) JIT 在優(yōu)化假設(shè)階段做的假設(shè),執(zhí)行階段發(fā)現(xiàn)是不正確的時(shí)候,就會發(fā)生這種情況。比如當(dāng)循環(huán)中發(fā)現(xiàn)本次循環(huán)所使用的變量類型和上次循環(huán)的類型不一樣,或者原型鏈中插入了新的函數(shù),都會使 JIT 拋棄已優(yōu)化的代碼。
反優(yōu)化過程有兩部分開銷。第一,需要花時(shí)間丟掉已優(yōu)化的代碼并且回到基線版本。第二,如果函數(shù)依舊頻繁被調(diào)用,JIT 可能會再次把它發(fā)送到優(yōu)化編譯器,又做一次優(yōu)化編譯,這是在做無用功。
在 WebAssembly 中,類型都是確定了的,所以 JIT 不需要根據(jù)變量的類型做優(yōu)化假設(shè)。也就是說 WebAssembly 沒有重優(yōu)化階段。
執(zhí)行
自己也可以寫出執(zhí)行效率很高的 JavaScript 代碼。你需要了解 JIT 的優(yōu)化機(jī)制,例如你要知道什么樣的代碼編譯器會對其進(jìn)行特殊處理(JIT 文章里面有提到過)。
然而大多數(shù)的開發(fā)者是不知道 JIT 內(nèi)部的實(shí)現(xiàn)機(jī)制的。即使開發(fā)者知道 JIT 的內(nèi)部機(jī)制,也很難寫出符合 JIT 標(biāo)準(zhǔn)的代碼,因?yàn)槿藗兺ǔ榱舜a可讀性更好而使用的編碼模式,恰恰不合適編譯器對代碼的優(yōu)化。
加之 JIT 會針對不同的瀏覽器做不同的優(yōu)化,所以對于一個(gè)瀏覽器優(yōu)化的比較好,很可能在另外一個(gè)瀏覽器上執(zhí)行效率就比較差。
正是因?yàn)檫@樣,執(zhí)行 WebAssembly 通常會比較快,很多 JIT 為 JavaScript 所做的優(yōu)化在 WebAssembly 并不需要。另外,WebAssembly 就是為了編譯器而設(shè)計(jì)的,開發(fā)人員不直接對其進(jìn)行編程,這樣就使得 WebAssembly 專注于提供更加理想的指令(執(zhí)行效率更高的指令)給機(jī)器就好了。
執(zhí)行效率方面,不同的代碼功能有不同的效果,一般來講執(zhí)行效率會提高 10% - 800%。
垃圾回收
JavaScript 中,開發(fā)者不需要手動清理內(nèi)存中不用的變量。JS 引擎會自動地做這件事情,這個(gè)過程叫做垃圾回收。
可是,當(dāng)你想要實(shí)現(xiàn)性能可控,垃圾回收可能就是個(gè)問題了。垃圾回收器會自動開始,這是不受你控制的,所以很有可能它會在一個(gè)不合適的時(shí)機(jī)啟動。目前的大多數(shù)瀏覽器已經(jīng)能給垃圾回收安排一個(gè)合理的啟動時(shí)間,不過這還是會增加代碼執(zhí)行的開銷。
目前為止,WebAssembly 不支持垃圾回收。內(nèi)存操作都是手動控制的(像 C、C++一樣)。這對于開發(fā)者來講確實(shí)增加了些開發(fā)成本,不過這也使代碼的執(zhí)行效率更高。
總結(jié)
WebAssembly 比 JavaScript 執(zhí)行更快是因?yàn)?#xff1a;
- 文件抓取階段,WebAssembly 比 JavaScript 抓取文件更快。即使 JavaScript 進(jìn)行了壓縮,WebAssembly 文件的體積也比 JavaScript 更小;
- 解析階段,WebAssembly 的解碼時(shí)間比 JavaScript 的解析時(shí)間更短;
- 編譯和優(yōu)化階段,WebAssembly 更具優(yōu)勢,因?yàn)?WebAssembly 的代碼更接近機(jī)器碼,而 JavaScript 要先通過服務(wù)器端進(jìn)行代碼優(yōu)化。
- 重優(yōu)化階段,WebAssembly 不會發(fā)生重優(yōu)化現(xiàn)象。而 JS 引擎的優(yōu)化假設(shè)則可能會發(fā)生“拋棄優(yōu)化代碼<->重優(yōu)化”現(xiàn)象。
- 執(zhí)行階段,WebAssembly 更快是因?yàn)殚_發(fā)人員不需要懂太多的編譯器技巧,而這在 JavaScript 中是需要的。WebAssembly 代碼也更適合生成機(jī)器執(zhí)行效率更高的指令。
- 垃圾回收階段,WebAssembly 垃圾回收都是手動控制的,效率比自動回收更高。
這就是為什么在大多數(shù)情況下,同一個(gè)任務(wù) WebAssembly 比 JavaScript 表現(xiàn)更好的原因。
但是,還有一些情況 WebAssembly 表現(xiàn)的會不如預(yù)期;同時(shí) WebAssembly 的未來也會朝著使 WebAssembly 執(zhí)行效率更高的方向發(fā)展。這些我會在下一篇文章《WebAssembly 系列(六)WebAssembly 的現(xiàn)在與未來》中介紹。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點(diǎn)。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的WebAssembly 系列(五)为什么 WebAssembly 更快?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。