WebAssembly 系列(一)生動形象地介紹 WebAssembly
WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理
WebAssembly 系列(三)編譯器如何生成匯編
WebAssembly 系列(四)WebAssembly 工作原理
WebAssembly 系列(五)為什么 WebAssembly 更快?
WebAssembly 系列(六)WebAssembly 的現(xiàn)在與未來
如何評論瀏覽器最新的 WebAssembly 字節(jié)碼技術(shù)?
WebAssembly:解決 JavaScript 痼疾的銀彈泡仗?
WebAssembly讓你的Javascript計(jì)算性能提升70%
現(xiàn)在的JavaScript代碼要進(jìn)行性能優(yōu)化共虑,通常使用一些常規(guī)手段掖疮,如:延遲執(zhí)行心软、預(yù)處理癞蚕、setTimeout等異步方式避免處理主線程蕊爵,高大上一點(diǎn)的會使用WebWorker。即使對于WebWorker也僅僅是解決了阻塞主線程的問題桦山,但是對于JavaScript計(jì)算性能慢的問題并沒有解決攒射。在目前階段,WebAssembly 適合大量密集計(jì)算恒水、并且無需頻繁與 JavaScript 及 DOM 進(jìn)行數(shù)據(jù)通訊的場景会放。比如游戲渲染引擎、物理引擎钉凌、圖像音頻視頻處理編輯咧最、加密算法等。
WebAssembly是一種運(yùn)行在現(xiàn)代網(wǎng)絡(luò)瀏覽器中的新型代碼并且提供新的性能特性和效果。它設(shè)計(jì)的目的不是為了手寫代碼而是為諸如C窗市、C++和Rust等低級源語言提供一個高效的編譯目標(biāo)先慷。WebAssembly的模塊可以被導(dǎo)入的到一個網(wǎng)絡(luò)app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函數(shù)咨察。JavaScript框架不但可以使用WebAssembly獲得巨大性能優(yōu)勢和新特性论熙,而且還能使得各種功能保持對網(wǎng)絡(luò)開發(fā)者的易用性。簡單來說WebAssembly就是瀏覽器提供的一項(xiàng)直接運(yùn)行二進(jìn)制機(jī)器代碼的能力摄狱。這些機(jī)器代碼怎么來呢脓诡,是通過C、C++或Rust等語言編譯來的媒役。
一祝谚、JIT(Just-in-time compilation)
JavaScript 于 1995 年問世,它的設(shè)計(jì)初衷并不是為了執(zhí)行起來快酣衷,在前 10 個年頭交惯,它的執(zhí)行速度也確實(shí)不快。緊接著穿仪,瀏覽器市場競爭開始激烈起來席爽。被人們廣為傳播的“性能大戰(zhàn)”在 2008 年打響。許多瀏覽器引入了 Just-in-time 編譯器啊片,也叫 JIT只锻。基于 JIT 的模式紫谷,JavaScript 代碼的運(yùn)行漸漸變快齐饮。正是由于這些 JIT 的引入,使得 JavaScript 的性能達(dá)到了一個轉(zhuǎn)折點(diǎn)笤昨,JS 代碼執(zhí)行速度快了 10 倍祖驱。
在代碼的世界中,通常有兩種方式來翻譯機(jī)器語言:解釋器和編譯器咬腋。如果是通過解釋器羹膳,翻譯是一行行地邊解釋邊執(zhí)行睡互。編譯器是把源代碼整個編譯成目標(biāo)代碼根竿,執(zhí)行時不再需要編譯器,直接在支持目標(biāo)代碼的平臺上運(yùn)行就珠。這兩種翻譯的方式都各有利弊寇壳。
1.解釋器的利弊
解釋器啟動和執(zhí)行的更快。你不需要等待整個編譯過程完成就可以運(yùn)行你的代碼妻怎。從第一行開始翻譯壳炎,就可以依次繼續(xù)執(zhí)行了。正是因?yàn)檫@個原因,解釋器看起來更加適合 JavaScript匿辩。對于一個 Web 開發(fā)人員來講腰耙,能夠快速執(zhí)行代碼并看到結(jié)果是非常重要的。這就是為什么最開始的瀏覽器都是用 JavaScript 解釋器的原因铲球。
可是當(dāng)你運(yùn)行同樣的代碼一次以上的時候挺庞,解釋器的弊處就顯現(xiàn)出來了。比如你執(zhí)行一個循環(huán)稼病,那解釋器就不得不一次又一次的進(jìn)行翻譯选侨,這是一種效率低下的表現(xiàn)。
2.編譯器的利弊
編譯器的問題則恰好相反然走。
它需要花一些時間對整個源代碼進(jìn)行編譯援制,然后生成目標(biāo)文件才能在機(jī)器上執(zhí)行。對于有循環(huán)的代碼執(zhí)行的很快芍瑞,因?yàn)樗恍枰貜?fù)的去翻譯每一次循環(huán)晨仑。
另外一個不同是,編譯器可以用更多的時間對代碼進(jìn)行優(yōu)化拆檬,以使的代碼執(zhí)行的更快寻歧。而解釋器是在 runtime 時進(jìn)行這一步驟的,這就決定了它不可能在翻譯的時候用很多時間進(jìn)行優(yōu)化秩仆。
3.JIT綜合了兩者的優(yōu)點(diǎn)
為了解決解釋器的低效問題码泛,后來的瀏覽器把編譯器也引入進(jìn)來,形成混合模式澄耍。不同的瀏覽器實(shí)現(xiàn)這一功能的方式不同噪珊,不過其基本思想是一致的。在 JavaScript 引擎中增加一個監(jiān)視器(也叫分析器)齐莲。監(jiān)視器監(jiān)控著代碼的運(yùn)行情況痢站,記錄代碼一共運(yùn)行了多少次、如何運(yùn)行的等信息选酗。如果同一行代碼運(yùn)行了幾次阵难,這個代碼段就被標(biāo)記成了 “warm”,如果運(yùn)行了很多次芒填,則被標(biāo)記成 “hot”呜叫。
4.基線編譯器
如果一段代碼變成了 “warm”,那么 JIT 就把它送到編譯器去編譯殿衰,并且把編譯結(jié)果存儲起來朱庆。代碼段的每一行都會被編譯成一個“樁”(stub),同時給這個樁分配一個以“行號 + 變量類型”的索引闷祥。如果監(jiān)視器監(jiān)視到了執(zhí)行同樣的代碼和同樣的變量類型娱颊,那么就直接把這個已編譯的版本 push 出來給瀏覽器。
5.優(yōu)化編譯器
如果一個代碼段變得 “very hot”,監(jiān)視器會把它發(fā)送到優(yōu)化編譯器中箱硕。生成一個更快速和高效的代碼版本出來拴竹,并且存儲之。
6.去優(yōu)化
整個優(yōu)化器起作用的鏈條是這樣的剧罩,監(jiān)視器從他所監(jiān)視代碼的執(zhí)行情況做出自己的判斷殖熟,接下來把它所整理的信息傳遞給優(yōu)化器進(jìn)行優(yōu)化。如果某個循環(huán)中先前每次迭代的對象都有相同的形狀斑响,那么就可以認(rèn)為它以后迭代的對象的形狀都是相同的菱属。可是對于 JavaScript 從來就沒有保證這么一說舰罚,前 99 個對象保持著形狀纽门,可能第 100 個就少了某個屬性。
正是由于這樣的情況营罢,所以編譯代碼需要在運(yùn)行之前檢查其假設(shè)是不是合理的赏陵。如果合理,那么優(yōu)化的編譯代碼會運(yùn)行饲漾,如果不合理蝙搔,那么 JIT 會認(rèn)為做了一個錯誤的假設(shè),并且把優(yōu)化代碼丟掉考传。這時(發(fā)生優(yōu)化代碼丟棄的情況)執(zhí)行過程將會回到解釋器或者基線編譯器吃型,這一過程叫做去優(yōu)化。
通常優(yōu)化編譯器會使得代碼變得更快僚楞,但是一些情況也會引起一些意想不到的性能問題勤晚。如果你的代碼一直陷入優(yōu)化<->去優(yōu)化的怪圈,那么程序執(zhí)行將會變慢泉褐,還不如基線編譯器快赐写。大多數(shù)的瀏覽器都做了限制,當(dāng)優(yōu)化/去優(yōu)化循環(huán)發(fā)生的時候會嘗試跳出這種循環(huán)膜赃。比如挺邀,如果 JIT 做了 10 次以上的優(yōu)化并且又丟棄的操作,那么就不繼續(xù)嘗試去優(yōu)化這段代碼了樁跳座。
7.總結(jié)
簡而言之 JIT 是什么呢端铛?它是使 JavaScript 運(yùn)行更快的一種手段,通過監(jiān)視代碼的運(yùn)行狀態(tài)躺坟,把 hot 代碼(重復(fù)執(zhí)行多次的代碼)進(jìn)行優(yōu)化沦补。通過這種方式,可以使 JavaScript 應(yīng)用的性能提升很多倍咪橙。為了使執(zhí)行速度變快,JIT 會增加很多多余的開銷,這些開銷包括:
- 優(yōu)化和去優(yōu)化開銷
- 監(jiān)視器記錄信息對內(nèi)存的開銷
- 發(fā)生去優(yōu)化情況時恢復(fù)信息的記錄對內(nèi)存的開銷
- 對基線版本和優(yōu)化后版本記錄的內(nèi)存開銷
下面這張圖片介紹了性能使用的大概分布情況美侦。
圖中的每一個顏色條都代表了不同的任務(wù):
- Parsing——表示把源代碼變成解釋器可以運(yùn)行的代碼所花的時間产舞;
- Compiling + optimizing——表示基線編譯器和優(yōu)化編譯器花的時間。一些優(yōu)化編譯器的工作并不在主線程運(yùn)行菠剩,不包含在這里易猫。
- Re-optimizing——當(dāng) JIT 發(fā)現(xiàn)優(yōu)化假設(shè)錯誤,丟棄優(yōu)化代碼所花的時間具壮。包括重優(yōu)化的時間准颓、拋棄并返回到基線編譯器的時間。
- Execution——執(zhí)行代碼的時間
- Garbage collection——垃圾回收棺妓,清理內(nèi)存的時間
這里注意:這些任務(wù)并不是離散執(zhí)行的攘已,或者按固定順序依次執(zhí)行的。而是交叉執(zhí)行怜跑,比如正在進(jìn)行解析過程時样勃,其他一些代碼正在運(yùn)行,而另一些正在編譯性芬。
二峡眶、asm.js
JIT 基于運(yùn)行期分析編譯,而 Javascript 是一個沒有類型的語言植锉,于是辫樱, 大部分時間,JIT 編譯器其實(shí)是在猜測 Javascript 中的類型俊庇。既然 JIT 遇到的問題是類型不確定問題和有一些語言功能搏熄,比如異常,for in 暇赤, JIT 起來很麻煩心例, 我可不可以搞個方法讓用戶不去用這些功能,同時讓他們把用的類型都標(biāo)注出來啊鞋囊。按照這個思路止后, 催生了兩種實(shí)現(xiàn)路徑:
一種是 Typescript, Dart, JSX 為代表的,基本思想是溜腐, 我搞個其他的語言译株,這個語言是強(qiáng)類型的,所以程序猿們需要指定類型挺益,然后我把它編譯成 Javacript 不就行了嘛歉糜。強(qiáng)類型的語言編譯成弱類型還不容易,什么望众,不知道怎么編匪补? 把類型去掉就行了嘛伞辛。
另一種是火狐的 Asm.js 為代表的, 做一個 javascript 子集夯缺, 同時試圖利用標(biāo)注的方法蚤氏,加上變量類型。這是 Mozilla 在 2013 年推出的一項(xiàng)新技術(shù)踊兜,舍棄了大量會導(dǎo)致性能問題的語法竿滨,并且被設(shè)計(jì)為通過 C / C++ 代碼編譯生成,而非手工編寫 asm.js 代碼捏境。上述的 sum 函數(shù)在 asm.js 中表現(xiàn)為:
function sum ( a ,b ) {
a = a | 0;
b = b | 0;
return ( a + b ) | 0;
}
上述代碼中于游,標(biāo)準(zhǔn)的 JavaScript 引擎會對其進(jìn)行解析,并生成正確的結(jié)果垫言,而 asm.js 會根據(jù)一些不會對運(yùn)行時造成計(jì)算結(jié)果錯誤的特殊標(biāo)識對變量的類型進(jìn)行聲明(比如 a = a | 0 表示變量 a 是一個整數(shù))贰剥,通過這種方式,這種代碼既可以在支持 asm.js 的 JavaScript 引擎上得到很高的性能骏掀,也會在不支持的設(shè)備上繼續(xù)按照正確的邏輯進(jìn)行執(zhí)行鸠澈,而非無法運(yùn)行。雖然如此截驮,asm.js 仍然存在著一些問題笑陈,主要是基于 JavaScript 語法的文本格式解析速度不夠快,并且代碼尺寸偏大葵袭。
如果你沒有注意到涵妥,第二種的速度提升潛力比第一種要大非常多。因?yàn)榈谝环N坡锡,無論如何蓬网,也是就是讓JIT (即時編譯) 快一點(diǎn), 第二種那可直接就編譯了啊 (AOT).
Web Assembly 就是第二種方式,說到底鹉勒,Mozilla, Google, Microsoft, and Apple 覺得 Asm.js 這個方法有前途帆锋,想標(biāo)準(zhǔn)化一下,大家都能用禽额。有了大佬們的支持锯厢,Web Assembly 比 asm.js 要激進(jìn)很多。 Web Assembly 連標(biāo)注 Js 這種事情都懶得做了脯倒,不是要 AOT 嗎实辑? 我直接給字節(jié)碼好不好?(后來改成 AST 樹)藻丢。對于不支持 Web Assembly 的瀏覽器剪撬, 會有一段 Javascript 把 Web Assembly 重新翻譯為 Javascript 運(yùn)行, 這個技術(shù)叫 polyfill, HTML5 剛出來的時候很常用的一個技術(shù)悠反。使用 AST 的原因是因?yàn)?AST 比字節(jié)碼更容易壓縮残黑,也更容易翻譯馍佑。
上圖展示了如何通過編寫 C / C++ 代碼生成 WebAssembly 內(nèi)容。
首先通過 LLVM 萍摊,將 C/C++ 源代碼編譯為 LLVM bytecode挤茄。這 是一種跨語言的底層虛擬機(jī)字節(jié)碼如叼,理論上所有強(qiáng)類型編程語言均可以生成這種字節(jié)碼冰木。通過這一點(diǎn)可以得知,在未來理論上所有強(qiáng)類型編程語言(諸如 Java / C# 等)均可以開發(fā) WebAssembly 程序笼恰。
其次踊沸,通過 EMScripten 中的后端編譯器,將這種抽象字節(jié)碼生成 asm.js 格式的文件社证。這是一種特殊的 JavaScript 代碼逼龟,部分 JavaScript 引擎會將這種格式以比通常的 JavaScript 代碼更快的速度運(yùn)行,并且由于 asm.js 仍然是 JavaScript追葡,所以哪怕 JavaScript 引擎不支持該特性腺律,也會以通常的方式運(yùn)行這段邏輯。這意味著使用 C/C++ 編寫的源代碼宜肉,哪怕用戶設(shè)備不支持 WebAssembly匀钧,也可以回退到 JavaScript 運(yùn)行并得到一致的結(jié)果。
第三谬返,asm.js 會通過另一個編譯器生成為 WebAssembly 的 .wasm 文件之斯,由于 WebAssembly 是二進(jìn)制格式,相比 JavaScript 而言遣铝,其代碼體積同比小很多佑刷,并且由于已經(jīng)是面向機(jī)器碼的格式,也無需在運(yùn)行前對源代碼耗費(fèi)時間進(jìn)行 JIT 編譯操作酿炸。
通過上述內(nèi)容可以看出瘫絮,WebAssembly 理論上可以通過任何強(qiáng)類型語言生成,不強(qiáng)制依賴用戶的本地運(yùn)行環(huán)境填硕,代碼體積小麦萤、解析速度快,幾乎是 Web 開發(fā)未來的一顆“銀色子彈”廷支。
三频鉴、WebAssembly
機(jī)器碼的翻譯并不是只有一種,不同的機(jī)器有不同的機(jī)器碼恋拍,就像我們?nèi)祟愐舱f各種各樣的語言一樣垛孔,機(jī)器也“說”不同的語言。人類和外星人之間的語言翻譯施敢,可能會從英語周荐、德語或中文翻譯到外星語 A 或者外星語 B狭莱。而在程序的世界里,則是從 C概作、C++ 或者 JAVA 翻譯到 x86 或者 ARM腋妙。
你想要從任意一個高級語言翻譯到眾多匯編語言中的一種(依賴機(jī)器內(nèi)部結(jié)構(gòu)),其中一種方式是創(chuàng)建不同的翻譯器來完成各種高級語言到匯編的映射讯榕。
這種翻譯的效率實(shí)在太低了骤素。為了解決這個問題,大多數(shù)編譯器都會在中間多加一層愚屁。它會把高級語言翻譯到一個低層济竹,而這個低層又沒有低到機(jī)器碼這個層級。這就是中間代碼( intermediate representation霎槐,IR)送浊。
這就是說編譯器會把高級語言翻譯到 IR 語言,而編譯器另外的部分再把 IR 語言編譯成特定目標(biāo)結(jié)構(gòu)的可執(zhí)行代碼丘跌。
重新總結(jié)一下:編譯器的前端把高級語言翻譯到 IR袭景,編譯器的后端把 IR 翻譯成目標(biāo)機(jī)器的匯編代碼。
那么在上圖中闭树,WebAssembly 在什么位置呢耸棒?實(shí)際上,你可以把它看成另一種“目標(biāo)匯編語言”蔼啦。WebAssembly 與其他的匯編語言不一樣榆纽,它不依賴于具體的物理機(jī)器∧笾可以抽象地理解成它是概念機(jī)器的機(jī)器語言奈籽,而不是實(shí)際的物理機(jī)器的機(jī)器語言。正因?yàn)槿绱送液眨琖ebAssembly 指令有時也被稱為虛擬指令衣屏。它比 JavaScript 代碼更直接地映射到機(jī)器碼,它也代表了“如何能在通用的硬件上更有效地執(zhí)行代碼”的一種理念辩棒。所以它并不直接映射成特定硬件的機(jī)器碼狼忱。
目前對于 WebAssembly 支持情況最好的編譯器工具鏈?zhǔn)?LLVM。有很多不同的前端和后端插件可以用在 LLVM 上一睁。還有一個易用的工具钻弄,叫做 Emscripten。它通過自己的后端先把代碼轉(zhuǎn)換成自己的中間代碼(叫做 asm.js)者吁,然后再轉(zhuǎn)化成 WebAssembly窘俺。實(shí)際上它背后也是使用的 LLVM。Emscripten 還包含了許多額外的工具和庫來包容整個 C/C++ 代碼庫复凳,所以它更像是一個軟件開發(fā)者工具包(SDK)而不是編譯器瘤泪。例如系統(tǒng)開發(fā)者需要文件系統(tǒng)以對文件進(jìn)行讀寫灶泵,Emscripten 就有一個 IndexedDB 來模擬文件系統(tǒng)。不考慮太多的這些工具鏈对途,只要知道最終生成了 .wasm 文件就可以了赦邻。
四、為什么 WebAssembly 更快
1.文件獲取
這一步并沒有顯示在圖表中实檀,但是這看似簡單地從服務(wù)器獲取文件這個步驟惶洲,卻會花費(fèi)很長時間。WebAssembly 比 JavaScript 的壓縮率更高劲妙,所以文件獲取也更快湃鹊。即便通過壓縮算法可以顯著地減小 JavaScript 的包大小儒喊,但是壓縮后的 WebAssembly 的二進(jìn)制代碼依然更小镣奋。這就是說在服務(wù)器和客戶端之間傳輸文件更快,尤其在網(wǎng)絡(luò)不好的情況下怀愧。
2.解析
當(dāng)?shù)竭_(dá)瀏覽器時侨颈,JavaScript 源代碼就被解析成了抽象語法樹。瀏覽器采用懶加載的方式進(jìn)行芯义,只解析真正需要的部分哈垢,而對于瀏覽器暫時不需要的函數(shù)只保留它的樁(stub,譯者注:關(guān)于樁的解釋可以在之前的文章中有提及)扛拨。而 WebAssembly 則不需要這種轉(zhuǎn)換耘分,因?yàn)樗旧砭褪侵虚g代碼。它要做的只是解碼并且檢查確認(rèn)代碼沒有錯誤就可以了绑警。
3.編譯和優(yōu)化
在關(guān)于 JIT 的文章中求泰,我有介紹過,JavaScript是在代碼的執(zhí)行階段編譯的计盒。因?yàn)樗侨躅愋驼Z言渴频,當(dāng)變量類型發(fā)生變化時,同樣的代碼會被編譯成不同版本北启。不同瀏覽器處理 WebAssembly 的編譯過程也不同卜朗,有些瀏覽器只對 WebAssembly 做基線編譯,而另一些瀏覽器用 JIT 來編譯咕村。不論哪種方式场钉,WebAssembly 都更貼近機(jī)器碼,所以它更快懈涛,使它更快的原因有幾個:
- 在編譯優(yōu)化代碼之前逛万,它不需要提前運(yùn)行代碼以知道變量都是什么類型。
- 編譯器不需要對同樣的代碼做不同版本的編譯肩钠。
- 很多優(yōu)化在 LLVM 階段就已經(jīng)做完了泣港,所以在編譯和優(yōu)化的時候沒有太多的優(yōu)化需要做暂殖。
4.重優(yōu)化
有些情況下,JIT 會反復(fù)地進(jìn)行“拋棄優(yōu)化代碼<->重優(yōu)化”過程当纱。當(dāng) JIT在優(yōu)化假設(shè)階段做的假設(shè)呛每,執(zhí)行階段發(fā)現(xiàn)是不正確的時候,就會發(fā)生這種情況坡氯。比如當(dāng)循環(huán)中發(fā)現(xiàn)本次循環(huán)所使用的變量類型和上次循環(huán)的類型不一樣晨横,或者原型鏈中插入了新的函數(shù),都會使 JIT 拋棄已優(yōu)化的代碼箫柳。
反優(yōu)化過程有兩部分開銷手形。第一,需要花時間丟掉已優(yōu)化的代碼并且回到基線版本悯恍。第二库糠,如果函數(shù)依舊頻繁被調(diào)用,JIT 可能會再次把它發(fā)送到優(yōu)化編譯器涮毫,又做一次優(yōu)化編譯瞬欧,這是在做無用功。在 WebAssembly 中罢防,類型都是確定了的艘虎,所以 JIT 不需要根據(jù)變量的類型做優(yōu)化假設(shè)。也就是說 WebAssembly 沒有重優(yōu)化階段咒吐。
5.執(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)化绪励,所以對于一個瀏覽器優(yōu)化的比較好肿孵,很可能在另外一個瀏覽器上執(zhí)行效率就比較差。
正是因?yàn)檫@樣疏魏,執(zhí)行 WebAssembly 通常會比較快停做,很多 JIT 為 JavaScript 所做的優(yōu)化在 WebAssembly 并不需要。另外大莫,WebAssembly 就是為了編譯器而設(shè)計(jì)的蛉腌,開發(fā)人員不直接對其進(jìn)行編程,這樣就使得 WebAssembly 專注于提供更加理想的指令(執(zhí)行效率更高的指令)給機(jī)器就好了。執(zhí)行效率方面烙丛,不同的代碼功能有不同的效果舅巷,一般來講執(zhí)行效率會提高 10% - 800%。
6.垃圾回收
JavaScript 中河咽,開發(fā)者不需要手動清理內(nèi)存中不用的變量钠右。JS 引擎會自動地做這件事情,這個過程叫做垃圾回收忘蟹§浚可是,當(dāng)你想要實(shí)現(xiàn)性能可控媚值,垃圾回收可能就是個問題了狠毯。垃圾回收器會自動開始,這是不受你控制的褥芒,所以很有可能它會在一個不合適的時機(jī)啟動嚼松。目前的大多數(shù)瀏覽器已經(jīng)能給垃圾回收安排一個合理的啟動時間,不過這還是會增加代碼執(zhí)行的開銷喂很。
目前為止惜颇,WebAssembly 不支持垃圾回收。內(nèi)存操作都是手動控制的(像 C少辣、C++一樣)。這對于開發(fā)者來講確實(shí)增加了些開發(fā)成本羡蛾,不過這也使代碼的執(zhí)行效率更高漓帅。
五、WebAssembly 優(yōu)缺點(diǎn)
1.中間調(diào)用的開銷
目前痴怨,在 JS 中調(diào)用 WebAssembly 的速度比本應(yīng)達(dá)到的速度要慢忙干。這是因?yàn)橹虚g需要做一次“蹦床運(yùn)動”。JIT 沒有辦法直接處理 WebAssembly浪藻,所以 JIT 要先把 WebAssembly 函數(shù)發(fā)送到懂它的地方捐迫。這一過程是引擎中比較慢的地方。如果你傳遞的是單一任務(wù)給 WebAssembly 模塊爱葵,那么不用擔(dān)心這個開銷施戴,因?yàn)橹挥幸淮无D(zhuǎn)換,也會比較快萌丈。但是如果是頻繁地從 WebAssembly 和 JavaScript 之間切換赞哗,那么這個開銷就必須要考慮了。
2.快速加載
JIT 必須要在快速加載和快速執(zhí)行之間做權(quán)衡辆雾。如果在編譯和優(yōu)化階段花了大量的時間肪笋,那么執(zhí)行的必然會很快,但是啟動會比較慢。目前有大量的工作正在研究藤乙,如何使預(yù)編譯時間和程序真正執(zhí)行時間兩者平衡猜揪。
3.無法直接操作 DOM
目前 WebAssembly 沒有任何方法可以與 DOM 直接交互。就是說你還不能通過比如element.innerHTML 的方法來更新節(jié)點(diǎn)坛梁。想要操作 DOM湿右,必須要通過 JS。
目前 WebAssembly 類似 WebWorker 罚勾,只能進(jìn)行單純的數(shù)值計(jì)算工作毅人,不能在 C++ 層直接操作 DOM 節(jié)點(diǎn)。雖然在未來路線圖中提及這一特性會在后續(xù)加入尖殃,但是在目前階段 WebAssembly 更適合被用于更純粹的密集型數(shù)據(jù)計(jì)算工作丈莺,而非直接編寫業(yè)務(wù)邏輯。WebAssembly 更適合編寫應(yīng)用程序中對性能要求比較高的庫送丰,并與 JavaScript 編寫的業(yè)務(wù)邏輯進(jìn)行通訊缔俄,并在 JavaScript 端對 DOM 節(jié)點(diǎn)進(jìn)行操作。
以筆者最近開發(fā)的白鷺引擎 5.0 的渲染庫為例器躏,白鷺引擎對外提供 JavaScript API俐载,開發(fā)者編寫的 JavaScript 邏輯代碼會匯總為一組命令隊(duì)列發(fā)送給 WebAssembly 層,然后 WebAssembly 負(fù)責(zé)所有的計(jì)算工作登失,最終生成一組基于 WebGL 格式的數(shù)據(jù)流遏佣,最后 JavaScript 對這組數(shù)據(jù)流進(jìn)行簡單的解析并直接調(diào)用 DOM 的 WebGL 接口傳遞數(shù)據(jù)。
Egret Engine5.0可以直接將 H5 游戲代碼編譯成機(jī)器碼運(yùn)行揽浙,對比Egret Engine4.0版效率提升可達(dá)300%状婶。Egret Engine5.0團(tuán)隊(duì)進(jìn)行封閉開發(fā)期間,精心研磨著重重寫了引擎底層從而支持 WebAssembly技術(shù)馅巷,為開發(fā)者提供更好的性能膛虫。如果瀏覽器不支持 WebAssembly ,5.0版引擎能夠自動智能切換成正常 JavaScript 版本钓猬,開發(fā)者無需擔(dān)憂正常使用稍刀。
4.共享內(nèi)存的并發(fā)性
提升代碼執(zhí)行速度的一個方法是使代碼并行運(yùn)行,不過有時也會適得其反敞曹,因?yàn)椴煌木€程在同步的時候可能會花費(fèi)更多的時間账月。
5.調(diào)試不方便
目前在瀏覽器中調(diào)試 WebAssembly 就像調(diào)試匯編一樣,很少的開發(fā)者可以手動地把自己的源代碼和匯編代碼對應(yīng)起來异雁。我們在致力于開發(fā)出更加適合開發(fā)者調(diào)試源代碼的工具捶障。
WebAssembly 被設(shè)計(jì)為了一種開放的、可調(diào)試的程序纲刀,但目前無論是 Chrome 還是 FireFox 项炼,在調(diào)試方面還有很大的提升空間担平。由于在目前階段調(diào)試較為困難,所以用 WebAssembly 編寫業(yè)務(wù)邏輯代碼對研發(fā)來說還是很不方便的锭部。
6.無法處理垃圾回收
如果你能提前確定變量類型暂论,那就可以把你的代碼變成 WebAssembly,例如 TypeScript 代碼就可以編譯成 WebAssembly拌禾。但是現(xiàn)在的問題是 WebAssembly 沒辦法處理垃圾回收的問題取胎,WebAssembly 中的內(nèi)存操作都是手動的。所以 WebAssembly 會考慮提供方便的 GC 功能湃窍,以方便開發(fā)者使用闻蛀。
7.體積很小
代碼體積很小,我們將大約 300k 左右(壓縮后)JavaScript 邏輯改用 WebAssembly 重寫后您市,體積僅有 90k 左右觉痛。雖然使用 WebAssembly 需要引入一個 50k-100k 的 JavaScript 類庫作為基礎(chǔ)設(shè)施,但是總體來看資源尺寸的優(yōu)勢還是很大的茵休。
8.安全性有很大改善
由于代碼格式是二進(jìn)制薪棒、無法直接在瀏覽器中看到源碼,盡管理論上仍然可以通過逆向工程一定程度上得到原有的業(yè)務(wù)邏輯榕莺,但是由于開發(fā)者可以在編譯時使用了 -O3 等激進(jìn)的優(yōu)化策略俐芯,所以最終反編譯得到的業(yè)務(wù)邏輯也是很難閱讀的。雖然理論上一切在客戶端的內(nèi)容都是不安全的钉鸯,但是與所有代碼都直接暴露給用戶相比吧史,代碼安全性得到了很大的改善。
9.大量反復(fù)執(zhí)行的優(yōu)化不明顯亏拉,而普通業(yè)務(wù)邏輯又不便大量使用
在運(yùn)行 benchmark 等極限測試時扣蜻,游戲引擎使用 WebAssembly 并不比 JavaScript 有幾何量級的提升。筆者的推論是:由于 JavaScript 引擎的 JIT 機(jī)制會把經(jīng)常運(yùn)行的函數(shù)進(jìn)行極限的編譯優(yōu)化及塘,所以在 benchmark 這種代碼大量反復(fù)執(zhí)行的測試環(huán)境下,無論是 JavaScript 版本锐极,還是 WebAssembly 版本笙僚,運(yùn)行的都是高度優(yōu)化后的機(jī)器碼,雖然 WebAssembly 版本仍然比 JavaScript 版有一定的性能優(yōu)勢灵再,但是并不明顯肋层。
在運(yùn)行業(yè)務(wù)邏輯代碼時,由于大部分業(yè)務(wù)邏輯代碼只運(yùn)行一次翎迁,所以 JavaScript 引擎只會對這部分代碼進(jìn)行簡單的編譯優(yōu)化而非極限優(yōu)化栋猖,所以運(yùn)行這一部分代碼 WebAssembly 相比 JavaScript 版本而言提升巨大,但是因?yàn)樯衔乃鐾衾疲唤ㄗh開發(fā)者在編寫業(yè)務(wù)邏輯時使用 WebAssembly蒲拉,所以這里陷入了一個兩難。在目前而言,理想情況是除了底層庫之外雌团,部分關(guān)鍵的涉及性能問題的邏輯也可以使用 WebAssembly 進(jìn)行編寫燃领。