新聞中心
這篇文章主要是從JavaScript Just-in-time (JIT) 工作原理和編譯器如何生成匯編兩方面來(lái)了解 WebAssembly的獨(dú)特特性。

網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)公司!專(zhuān)注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、小程序開(kāi)發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶(hù)創(chuàng)新互聯(lián)還提供了蘭西免費(fèi)建站歡迎大家使用!
一、等等,什么是 WebAssembly?
WebAssembly 是除了 JavaScript 以外,另一種可以在瀏覽器中執(zhí)行的編程語(yǔ)言。所以當(dāng)人們說(shuō) WebAssembly 更快的時(shí)候,一般來(lái)講是與 JavaScript 相比而言的。
這里并不是暗示大家說(shuō)開(kāi)發(fā)時(shí)只能選擇 WebAssembly或 JavaScript。實(shí)際上,我們更希望在同一個(gè)工程中,兩個(gè)你同時(shí)使用。
對(duì)二者的比較倒是非常有必要的,這樣你就可以了解到 WebAssembly 所擁有的獨(dú)特特性。
二、一些關(guān)于性能的歷史
JavaScript 于 1995 年問(wèn)世,它的設(shè)計(jì)初衷并不是為了執(zhí)行起來(lái)快,在前 10 個(gè)年頭,它的執(zhí)行速度也確實(shí)不快。
緊接著,瀏覽器市場(chǎng)競(jìng)爭(zhēng)開(kāi)始激烈起來(lái)。
被人們廣為傳播的“性能大戰(zhàn)”在 2008 年打響。許多瀏覽器引入了 Just-in-time 編譯器,也叫 JIT?;?JIT 的模式,JavaScript 代碼的運(yùn)行漸漸變快。
正是由于這些 JIT 的引入,使得 JavaScript 的性能達(dá)到了一個(gè)轉(zhuǎn)折點(diǎn),JS 代碼執(zhí)行速度快了 10 倍。
隨著性能的提升,JavaScript 可以應(yīng)用到以前根本沒(méi)有想到過(guò)的領(lǐng)域,比如用于后端開(kāi)發(fā)的 Node.js。性能的提升使得 JavaScript 的應(yīng)用范圍得到很大的擴(kuò)展。
現(xiàn)在通過(guò) WebAssembly,我們很有可能正處于第二個(gè)拐點(diǎn)。
那么, JIT 是如何起作用的呢?下面來(lái)看。
三、JavaScript Just-in-time (JIT) 工作原理
1. JavaScript 在瀏覽器中是如何運(yùn)行的?
如果是你一個(gè)開(kāi)發(fā)者,當(dāng)你決定在你的頁(yè)面中使用 JavaScript 的時(shí)候,有兩個(gè)要考慮的事情:目標(biāo)和問(wèn)題。
- 目標(biāo):告訴計(jì)算機(jī)你想做什么。
- 問(wèn)題:你和計(jì)算機(jī)說(shuō)不同的語(yǔ)言,無(wú)法溝通。
你說(shuō)的是人類(lèi)的語(yǔ)言,而計(jì)算機(jī)用的是機(jī)器語(yǔ)言。機(jī)器語(yǔ)言也是一種語(yǔ)言,只是 JavaScript 或者其他高級(jí)編程語(yǔ)言機(jī)器能看得懂,而人類(lèi)不用他們來(lái)交流罷了。它們是基于人類(lèi)認(rèn)知而設(shè)計(jì)出來(lái)的。
所以呢,JavaScript 引擎的工作就是把人類(lèi)的語(yǔ)言轉(zhuǎn)換成機(jī)器能看懂的語(yǔ)言。
這就像電影《降臨》中,人類(lèi)和外星人的互相交流一樣。
在電影里面,人類(lèi)和外星人不僅僅是語(yǔ)言不同,兩個(gè)群體看待世界的方式都是不一樣的。其實(shí)人類(lèi)和機(jī)器也是類(lèi)似(后面我會(huì)詳細(xì)介紹)。
那么,翻譯是如何進(jìn)行的呢?
在代碼的世界中,通常有兩種方式來(lái)翻譯機(jī)器語(yǔ)言:解釋器和編譯器。
如果是通過(guò)解釋器,翻譯是一行行地邊解釋邊執(zhí)行
編譯器是把源代碼整個(gè)編譯成目標(biāo)代碼,執(zhí)行時(shí)不再需要編譯器,直接在支持目標(biāo)代碼的平臺(tái)上運(yùn)行。
這兩種翻譯的方式都各有利弊。
2. 解釋器的利弊
解釋器啟動(dòng)和執(zhí)行的更快。你不需要等待整個(gè)編譯過(guò)程完成就可以運(yùn)行你的代碼。從***行開(kāi)始翻譯,就可以依次繼續(xù)執(zhí)行了。
正是因?yàn)檫@個(gè)原因,解釋器看起來(lái)更加適合 JavaScript。對(duì)于一個(gè) Web 開(kāi)發(fā)人員來(lái)講,能夠快速執(zhí)行代碼并看到結(jié)果是非常重要的。
這就是為什么最開(kāi)始的瀏覽器都是用 JavaScript 解釋器的原因。
可是當(dāng)你運(yùn)行同樣的代碼一次以上的時(shí)候,解釋器的弊處就顯現(xiàn)出來(lái)了。比如你執(zhí)行一個(gè)循環(huán),那解釋器就不得不一次又一次的進(jìn)行翻譯,這是一種效率低下的表現(xiàn)。
3. 編譯器的利弊
編譯器的問(wèn)題則恰好相反。
它需要花一些時(shí)間對(duì)整個(gè)源代碼進(jìn)行編譯,然后生成目標(biāo)文件才能在機(jī)器上執(zhí)行。對(duì)于有循環(huán)的代碼執(zhí)行的很快,因?yàn)樗恍枰貜?fù)的去翻譯每一次循環(huán)。
另外一個(gè)不同是,編譯器可以用更多的時(shí)間對(duì)代碼進(jìn)行優(yōu)化,以使的代碼執(zhí)行的更快。而解釋器是在 runtime 時(shí)進(jìn)行這一步驟的,這就決定了它不可能在翻譯的時(shí)候用很多時(shí)間進(jìn)行優(yōu)化。
4. Just-in-time 編譯器:綜合了兩者的優(yōu)點(diǎn)
為了解決解釋器的低效問(wèn)題,后來(lái)的瀏覽器把編譯器也引入進(jìn)來(lái),形成混合模式。
不同的瀏覽器實(shí)現(xiàn)這一功能的方式不同,不過(guò)其基本思想是一致的。在 JavaScript 引擎中增加一個(gè)監(jiān)視器(也叫分析器)。監(jiān)視器監(jiān)控著代碼的運(yùn)行情況,記錄代碼一共運(yùn)行了多少次、如何運(yùn)行的等信息。
起初,監(jiān)視器監(jiān)視著所有通過(guò)解釋器的代碼。
如果同一行代碼運(yùn)行了幾次,這個(gè)代碼段就被標(biāo)記成了 “warm”,如果運(yùn)行了很多次,則被標(biāo)記成 “hot”。
5. 基線(xiàn)編譯器
如果一段代碼變成了 “warm”,那么 JIT 就把它送到編譯器去編譯,并且把編譯結(jié)果存儲(chǔ)起來(lái)。
代碼段的每一行都會(huì)被編譯成一個(gè)“樁”(stub),同時(shí)給這個(gè)樁分配一個(gè)以“行號(hào) + 變量類(lèi)型”的索引。如果監(jiān)視器監(jiān)視到了執(zhí)行同樣的代碼和同樣的變量類(lèi)型,那么就直接把這個(gè)已編譯的版本 push 出來(lái)給瀏覽器。
通過(guò)這樣的做法可以加快執(zhí)行速度,但是正如前面我所說(shuō)的,編譯器還可以找到更有效地執(zhí)行代碼的方法,也就是做優(yōu)化。
基線(xiàn)編譯器可以做一部分這樣的優(yōu)化(下面我會(huì)給出例子),不過(guò)基線(xiàn)編譯器優(yōu)化的時(shí)間不能太久,因?yàn)闀?huì)使得程序的執(zhí)行在這里 hold 住。
不過(guò)如果代碼確實(shí)非常 “hot”(也就是說(shuō)幾乎所有的執(zhí)行時(shí)間都耗費(fèi)在這里),那么花點(diǎn)時(shí)間做優(yōu)化也是值得的。
6. 優(yōu)化編譯器
如果一個(gè)代碼段變得 “very hot”,監(jiān)視器會(huì)把它發(fā)送到優(yōu)化編譯器中。生成一個(gè)更快速和高效的代碼版本出來(lái),并且存儲(chǔ)之。
為了生成一個(gè)更快速的代碼版本,優(yōu)化編譯器必須做一些假設(shè)。例如,它會(huì)假設(shè)由同一個(gè)構(gòu)造函數(shù)生成的實(shí)例都有相同的形狀——就是說(shuō)所有的實(shí)例都有相同的屬性名,并且都以同樣的順序初始化,那么就可以針對(duì)這一模式進(jìn)行優(yōu)化。
整個(gè)優(yōu)化器起作用的鏈條是這樣的,監(jiān)視器從他所監(jiān)視代碼的執(zhí)行情況做出自己的判斷,接下來(lái)把它所整理的信息傳遞給優(yōu)化器進(jìn)行優(yōu)化。如果某個(gè)循環(huán)中先前每次迭代的對(duì)象都有相同的形狀,那么就可以認(rèn)為它以后迭代的對(duì)象的形狀都是相同的。可是對(duì)于 JavaScript 從來(lái)就沒(méi)有保證這么一說(shuō),前 99 個(gè)對(duì)象保持著形狀,可能第 100 個(gè)就少了某個(gè)屬性。
正是由于這樣的情況,所以編譯代碼需要在運(yùn)行之前檢查其假設(shè)是不是合理的。如果合理,那么優(yōu)化的編譯代碼會(huì)運(yùn)行,如果不合理,那么 JIT 會(huì)認(rèn)為做了一個(gè)錯(cuò)誤的假設(shè),并且把優(yōu)化代碼丟掉。
這時(shí)(發(fā)生優(yōu)化代碼丟棄的情況)執(zhí)行過(guò)程將會(huì)回到解釋器或者基線(xiàn)編譯器,這一過(guò)程叫做去優(yōu)化。
通常優(yōu)化編譯器會(huì)使得代碼變得更快,但是一些情況也會(huì)引起一些意想不到的性能問(wèn)題。如果你的代碼一直陷入優(yōu)化<->去優(yōu)化的怪圈,那么程序執(zhí)行將會(huì)變慢,還不如基線(xiàn)編譯器快。
大多數(shù)的瀏覽器都做了限制,當(dāng)優(yōu)化/去優(yōu)化循環(huán)發(fā)生的時(shí)候會(huì)嘗試跳出這種循環(huán)。比如,如果 JIT 做了 10 次以上的優(yōu)化并且又丟棄的操作,那么就不繼續(xù)嘗試去優(yōu)化這段代碼了樁。
7. 一個(gè)優(yōu)化的例子:類(lèi)型特化(Type specialization)
有很多不同類(lèi)型的優(yōu)化方法,這里我介紹一種,讓大家能夠明白是如何優(yōu)化的。優(yōu)化編譯器最成功一個(gè)特點(diǎn)叫做類(lèi)型特化,下面詳細(xì)解釋。
JavaScript 所使用的動(dòng)態(tài)類(lèi)型體系在運(yùn)行時(shí)需要進(jìn)行額外的解釋工作,例如下面代碼:
- function arraySum(arr) {
- var sum = 0;
- for (var i = 0; i < arr.length; i++) {
- sum += arr[i];
- }
- }
+= 循環(huán)中這一步看起來(lái)很簡(jiǎn)單,只需要進(jìn)行一步計(jì)算,但是恰恰因?yàn)槭怯脛?dòng)態(tài)類(lèi)型,他所需要的步驟要比你所想象的更復(fù)雜一些。
我們假設(shè) arr 是一個(gè)有 100 個(gè)整數(shù)的數(shù)組。當(dāng)代碼被標(biāo)記為 “warm” 時(shí),基線(xiàn)編譯器就為函數(shù)中的每一個(gè)操作生成一個(gè)樁。sum += arr[i]會(huì)有一個(gè)相應(yīng)的樁,并且把里面的 += 操作當(dāng)成整數(shù)加法。
但是,sum 和 arr[i] 兩個(gè)數(shù)并不保證都是整數(shù)。因?yàn)樵?JavaScript 中類(lèi)型都是動(dòng)態(tài)類(lèi)型,在接下來(lái)的循環(huán)當(dāng)中,arr[i] 很有可能變成了string 類(lèi)型。整數(shù)加法和字符串連接是完全不同的兩個(gè)操作,會(huì)被編譯成不同的機(jī)器碼。
JIT 處理這個(gè)問(wèn)題的方法是編譯多基線(xiàn)樁。如果一個(gè)代碼段是單一形態(tài)的(即總是以同一類(lèi)型被調(diào)用),則只生成一個(gè)樁。如果是多形態(tài)的(即調(diào)用的過(guò)程中,類(lèi)型不斷變化),則會(huì)為操作所調(diào)用的每一個(gè)類(lèi)型組合生成一個(gè)樁。
這就是說(shuō) JIT 在選擇一個(gè)樁之前,會(huì)進(jìn)行多分枝選擇,類(lèi)似于決策樹(shù),問(wèn)自己很多問(wèn)題才會(huì)確定最終選擇哪個(gè),見(jiàn)下圖:
正是因?yàn)樵诨€(xiàn)編譯器中每行代碼都有自己的樁,所以 JIT 在每行代碼被執(zhí)行的時(shí)候都會(huì)檢查數(shù)據(jù)類(lèi)型。在循環(huán)的每次迭代,JIT 也都會(huì)重復(fù)一次分枝選擇。
如果代碼在執(zhí)行的過(guò)程中,JIT 不是每次都重復(fù)檢查的話(huà),那么執(zhí)行的還會(huì)更快一些,而這就是優(yōu)化編譯器所需要做的工作之一了。
優(yōu)化編譯器中,整個(gè)函數(shù)被統(tǒng)一編譯,這樣的話(huà)就可以在循環(huán)開(kāi)始執(zhí)行之前進(jìn)行類(lèi)型檢查。
一些瀏覽器的 JIT 優(yōu)化更加復(fù)雜。比如在 Firefox 中,給一些數(shù)組設(shè)定了特定的類(lèi)型,比如里面只包含整型。如果 arr 是這種數(shù)組類(lèi)型,那么 JIT 就不需要檢查 arr[i] 是不是整型了,這也意味著 JIT 可以在進(jìn)入循環(huán)之前進(jìn)行所有的類(lèi)型檢查。
8. 總結(jié)
簡(jiǎn)而言之 JIT 是什么呢?它是使 JavaScript 運(yùn)行更快的一種手段,通過(guò)監(jiān)視代碼的運(yùn)行狀態(tài),把 hot 代碼(重復(fù)執(zhí)行多次的代碼)進(jìn)行優(yōu)化。通過(guò)這種方式,可以使 JavaScript 應(yīng)用的性能提升很多倍。
為了使執(zhí)行速度變快,JIT 會(huì)增加很多多余的開(kāi)銷(xiāo),這些開(kāi)銷(xiāo)包括:
- 優(yōu)化和去優(yōu)化開(kāi)銷(xiāo)
- 監(jiān)視器記錄信息對(duì)內(nèi)存的開(kāi)銷(xiāo)
- 發(fā)生去優(yōu)化情況時(shí)恢復(fù)信息的記錄對(duì)內(nèi)存的開(kāi)銷(xiāo)
- 對(duì)基線(xiàn)版本和優(yōu)化后版本記錄的內(nèi)存開(kāi)銷(xiāo)
這里還有很大的提升空間:即消除開(kāi)銷(xiāo)。通過(guò)消除開(kāi)銷(xiāo)使得性能上有進(jìn)一步地提升,這也是 WebAssembly 所要做的事之一。
四、編譯器如何生成匯編
理解什么是匯編,以及編譯器如何生成它,對(duì)于理解 WebAssembly 是很有幫助的。
在上一篇關(guān)于 JIT 的文章中,我介紹了和計(jì)算機(jī)打交道,就像同外星人打交道一樣。
現(xiàn)在來(lái)思考一下“外星人”的大腦是如何工作的——機(jī)器的“大腦”是如何對(duì)我們輸入給它的內(nèi)容進(jìn)行分析和理解的。
“大腦”中,有一部分負(fù)責(zé)思考——處理加法、減法或者邏輯運(yùn)算。還有其他的部分分別負(fù)責(zé)短暫記憶和長(zhǎng)期記憶的。
這些不同的部分都有自己的名字:
- 負(fù)責(zé)思考的部分叫做算數(shù)邏輯單元(ALU)
- 寄存器提供短暫記憶功能
- 隨機(jī)存取存儲(chǔ)器(RAM)提供長(zhǎng)期記憶功能
機(jī)器代碼中的語(yǔ)句稱(chēng)作指令。
那么在指令進(jìn)入“大腦”以后都發(fā)生了什么呢?它們會(huì)被切分為不同的部分傳送到不同的單元進(jìn)行處理。
“大腦”切分指令通過(guò)不同連接線(xiàn)路進(jìn)行。舉個(gè)例子,“大腦”會(huì)將指令最開(kāi)始的 6 比特通過(guò)管道送到 ALU 中。而 ALU 會(huì)通過(guò) 0 和 1 的位置來(lái)決定對(duì)兩個(gè)數(shù)做加法。
這串 01 串就叫做“操作碼”,它告訴了 ALU 要執(zhí)行什么樣的操作。
然后“大腦”會(huì)取后面兩個(gè)連續(xù)的 3 比特 01 串來(lái)確定把哪兩個(gè)數(shù)加到一起,而這 3 比特指的是寄存器的地址。
注意看上面機(jī)器碼的注釋?zhuān)骸癆DD R1 R2”,這對(duì)于人類(lèi)來(lái)講很容易理解其含義。這就是匯編,也叫符號(hào)機(jī)器碼,它使人類(lèi)也能看懂機(jī)器代碼的含義。
可以看到匯編和這臺(tái)機(jī)器的機(jī)器碼之間有直接的映射關(guān)系。正是因?yàn)槿绱?,擁有不同機(jī)器結(jié)構(gòu)的計(jì)算機(jī)會(huì)有不同的匯編系統(tǒng)。如果你有一個(gè)機(jī)器,它有自己的內(nèi)部結(jié)構(gòu),那么它就需要它所獨(dú)有的匯編語(yǔ)言。
從上面的分析可以知道我們進(jìn)行機(jī)器碼的翻譯并不是只有一種,不同的機(jī)器有不同的機(jī)器碼,就像我們?nèi)祟?lèi)也說(shuō)各種各樣的語(yǔ)言一樣,機(jī)器也“說(shuō)”不同的語(yǔ)言。
人類(lèi)和外星人之間的語(yǔ)言翻譯,可能會(huì)從英語(yǔ)、德語(yǔ)或中文翻譯到外星語(yǔ) A 或者外星語(yǔ) B。而在程序的世界里,則是從 C、C++ 或者 JAVA 翻譯到 x86 或者 ARM。
你想要從任意一個(gè)高級(jí)語(yǔ)言翻譯到眾多匯編語(yǔ)言中的一種(依賴(lài)機(jī)器內(nèi)部結(jié)構(gòu)),其中一種方式是創(chuàng)建不同的翻譯器來(lái)完成各種高級(jí)語(yǔ)言到匯編的映射。
這種翻譯的效率實(shí)在太低了。為了解決這個(gè)問(wèn)題,大多數(shù)編譯器都會(huì)在中間多加一層。它會(huì)把高級(jí)語(yǔ)言翻譯到一個(gè)低層,而這個(gè)低層又沒(méi)有低到機(jī)器碼這個(gè)層級(jí)。這就是中間代碼( intermediate representation,IR)。
這就是說(shuō)編譯器會(huì)把高級(jí)語(yǔ)言翻譯到 IR 語(yǔ)言,而編譯器另外的部分再把 IR 語(yǔ)言編譯成特定目標(biāo)結(jié)構(gòu)的可執(zhí)行代碼。
重新總結(jié)一下:編譯器的前端把高級(jí)語(yǔ)言翻譯到 IR,編譯器的后端把 IR 翻譯成目標(biāo)機(jī)器的匯編代碼。
五、總結(jié)
本文介紹了 WebAssembly的相關(guān)知識(shí) ,在下一篇文章中,我們來(lái)介紹 WebAssembly 的工作原理。
點(diǎn)擊《WebAssembly 系列(一)生動(dòng)形象地介紹 WebAssembly》閱讀原文。
【本文是專(zhuān)欄作者“胡子大哈”的原創(chuàng)文章,轉(zhuǎn)載請(qǐng)聯(lián)系作者本人獲取授權(quán)】
戳這里,看該作者更多好文
新聞名稱(chēng):生動(dòng)形象地介紹WebAssembly
網(wǎng)站鏈接:http://www.5511xx.com/article/coghdsp.html


咨詢(xún)
建站咨詢(xún)
