摘要:編譯器優(yōu)缺點(diǎn)與解釋器相比,編譯器有著相反的優(yōu)缺點(diǎn)。它們?yōu)橐嫘略隽艘粋€(gè)組件,稱為監(jiān)視器,或者。優(yōu)化編譯器會(huì)基于監(jiān)視器記錄的代碼運(yùn)行信息來作出一些判斷。通常來說,優(yōu)化編譯器會(huì)使得代碼跑的更快。而這正是優(yōu)化編譯器所做的優(yōu)化之一。
本文是圖說 WebAssembly 系列文章的第二篇,如果你還沒閱讀其它的,建議您從第一篇開始。
JavaScript 的運(yùn)行,一開始是很慢的,但是后面會(huì)變得越來越快,背后的功臣就是 JIT 。
但是 JIT 是如何工作的呢?
作為開發(fā)者,我們給網(wǎng)頁(yè)寫 JavaScript 代碼是有明確目標(biāo)的,當(dāng)然也會(huì)伴隨著問題。
目標(biāo):告訴計(jì)算機(jī)要做什么。
問題:我們和計(jì)算機(jī)使用著不同語(yǔ)言。
我們使用的是人類語(yǔ)言,而計(jì)算機(jī)則使用機(jī)器語(yǔ)言。
雖然你可能不同意把 JavaScript 或者其他高級(jí)編程語(yǔ)言稱為人類語(yǔ)言,但它們也確確實(shí)實(shí)是人類語(yǔ)言。
因?yàn)樗鼈兪前凑杖祟愓J(rèn)知被設(shè)計(jì)出來的,而不是機(jī)器認(rèn)知。
所以,JavaScript 引擎的工作就是接收人類語(yǔ)言,然后輸出機(jī)器語(yǔ)言。
這就像電影《降臨》中所描述的一樣,人類試圖和外星人進(jìn)行交流。
電影中,人類和外星人的交流并不是通過逐個(gè)文字翻譯來實(shí)現(xiàn)的。這兩個(gè)群體有不同的世界觀,這種差異也同樣適用于人類和機(jī)器。
那么,人類和機(jī)器之間的“翻譯”又是怎么實(shí)現(xiàn)的呢?
在編程領(lǐng)域,翻譯成機(jī)器語(yǔ)言有兩種通用的方式:解釋器和編譯器。
使用解釋器時(shí),這種翻譯幾乎是實(shí)時(shí)且逐行進(jìn)行的。
而對(duì)于編譯器,卻不是實(shí)時(shí)的,它需要提前翻譯并保存起來。
這兩種翻譯方式各有利弊。
解釋器優(yōu)缺點(diǎn)解釋器可以快速啟動(dòng)并運(yùn)行代碼。
我們不需要等待整個(gè)編譯步驟結(jié)束之后才開始運(yùn)行代碼。翻譯一行,運(yùn)行一行。
基于此,解釋器看起來非常適合像 JavaScript 這樣的語(yǔ)言。因?yàn)閷?duì)于一個(gè)互聯(lián)網(wǎng)開發(fā)者來說,快速開始并運(yùn)行代碼是非常重要的。
這也是為什么瀏覽器從一開始就使用 JavaScript 解釋器的原因。
但是,當(dāng)需要多次運(yùn)行相同代碼時(shí),解釋器的弊端就凸顯出來了。
比如,在一個(gè)循環(huán)中,解釋器得重復(fù)的翻譯相同的代碼。
與解釋器相比,編譯器有著相反的優(yōu)缺點(diǎn)。
編譯器會(huì)在開始時(shí)耗費(fèi)比較多的時(shí)間,因?yàn)樗枰?jīng)歷整個(gè)編譯過程。不過,一旦編譯好,循環(huán)中的代碼就可以跑得更快,因?yàn)樗辉傩枰貜?fù)的翻譯相同代碼。
另一個(gè)不同點(diǎn)是,編譯器有更多的時(shí)間來分析代碼,然后修改代碼,使它能跑得更快。這個(gè)修改過程稱為優(yōu)化。
而解釋器就不同了,它是運(yùn)行時(shí)進(jìn)行代碼翻譯的,所以它沒法在這個(gè)過程中做優(yōu)化。
為了解決解釋器重復(fù)翻譯相同代碼低效行為,瀏覽器開始把編譯器引入進(jìn)來。
不同瀏覽器的做法有略微不同,但是基本做法是相同的。
它們?yōu)?JavaScript 引擎新增了一個(gè)組件,稱為監(jiān)視器(Monitor,或者 Profiler)。
監(jiān)視器的工作就是觀察代碼運(yùn)行,然后記錄代碼的運(yùn)行次數(shù),以及它們使用的數(shù)據(jù)類型。
最開始時(shí),監(jiān)視器會(huì)觀察解釋器運(yùn)行的所有代碼。
如果某一處的幾行代碼運(yùn)行了好幾次,那么該處的幾行代碼就被標(biāo)記為暖代碼(warm)。
如果運(yùn)行了非常多次,那么就會(huì)被標(biāo)記為熱代碼(hot) 。
當(dāng)一個(gè)函數(shù)被標(biāo)記為暖代碼,JIT 就會(huì)把它發(fā)送給基準(zhǔn)編譯器(Baseline Compiler)進(jìn)行編譯,并把編譯結(jié)果保存下來。
函數(shù)中的每一行代碼都被編譯成一個(gè)存根(Stub)。這些存根在存儲(chǔ)時(shí),使用代碼行號(hào)和變量類型作為索引。
如果監(jiān)視器發(fā)現(xiàn)相同的代碼運(yùn)行使用的是相同變量類型,那么它會(huì)取出已編譯好的代碼來運(yùn)行。
可以看出,這已經(jīng)加快了運(yùn)行速度。
不過,編譯器還可以做得更好。它可以花點(diǎn)時(shí)間來分析代碼,以便找出最高效的方式,也就是做優(yōu)化。
基準(zhǔn)編譯器也是能夠做一些優(yōu)化的(下文會(huì)舉例說明)。
但是它不能花費(fèi)太多時(shí)間在優(yōu)化上,因?yàn)槲覀儾⒉幌ML(zhǎng)時(shí)間阻塞代碼運(yùn)行。
當(dāng)有些代碼變成熱代碼,監(jiān)視器就會(huì)把它發(fā)送給優(yōu)化編譯器(Optimizing Compiler)。優(yōu)化編譯器會(huì)把它編譯成另一種更快版本的函數(shù),并且保存起來。
為了生成更快的代碼,優(yōu)化編譯器必須作出一些前提假設(shè)。
比如,如果假設(shè)使用特定構(gòu)造函數(shù)創(chuàng)建的對(duì)象都有相同的結(jié)構(gòu),即有相同的屬性名并且添加順序也是一致的,那么優(yōu)化編譯器就可以基于此刪除一些代碼。
優(yōu)化編譯器會(huì)基于監(jiān)視器記錄的代碼運(yùn)行信息來作出一些判斷。比如,如果在一個(gè)循環(huán)中,之前運(yùn)行時(shí)某個(gè)變量一直是 true,那么它就會(huì)假設(shè)它在未來仍然是 true。
當(dāng)然,在 JavaScript 中,其實(shí)是沒有任何保證可言的。
可能之前的 99 個(gè)對(duì)象都有著相同的結(jié)構(gòu),但是到第 100 個(gè)對(duì)象時(shí),它仍可能會(huì)缺少某個(gè)屬性。
因此,編譯后的代碼在運(yùn)行之前需要檢查原先的假設(shè)是否成立。
如果成立,那么直接運(yùn)行編譯后的代碼;如果不成立,那么 JIT 會(huì)認(rèn)為它作出了錯(cuò)誤假設(shè),于是它會(huì)把優(yōu)化的代碼廢棄掉。
然后,代碼的運(yùn)行會(huì)返回去使用解釋器運(yùn)行或者采用基準(zhǔn)編譯器編譯的代碼。這個(gè)過程稱為去優(yōu)化(Deoptimization)。
通常來說,優(yōu)化編譯器會(huì)使得代碼跑的更快。不過有時(shí)候,它也可能會(huì)導(dǎo)致意料之外的性能問題。
如果有一部分代碼一直在優(yōu)化和去優(yōu)化之間切換,那么它其實(shí)比直接使用基準(zhǔn)編譯器的編譯的代碼還更慢。
大多數(shù)瀏覽器已經(jīng)增加了一些限制,來及時(shí)打破這種優(yōu)化/去優(yōu)化的循環(huán)過程。
比如說,當(dāng) JIT 嘗試了 10 次優(yōu)化之后仍然發(fā)生了去優(yōu)化,那么它就不再嘗試對(duì)其進(jìn)行優(yōu)化。
有很多種不同的優(yōu)化方式,這里我們只舉例說明其中一種,來幫助理解整個(gè)優(yōu)化過程。
在眾多優(yōu)化方式中,類型特定化(Type Specialization)取得的優(yōu)化是最明顯的。
JavaScript 采用的動(dòng)態(tài)類型系統(tǒng)使得代碼在運(yùn)行時(shí)需要做些額外的檢查工作。
比如,對(duì)于以下代碼:
function arraySum(arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += arr[i]; } }
其中的 += 操作看起來非常簡(jiǎn)單,似乎只需要進(jìn)行一次操作就能完成計(jì)算。
但是,因?yàn)槭莿?dòng)態(tài)類型,實(shí)際上進(jìn)行的操作次數(shù)遠(yuǎn)不足一次那么簡(jiǎn)單。
讓我們假設(shè) arr 是一個(gè)包含 100 個(gè)整數(shù)的數(shù)組。一旦該函數(shù)被標(biāo)記為暖代碼,基準(zhǔn)編譯器就會(huì)為該函數(shù)中的每一個(gè)操作創(chuàng)建一個(gè)存根。所以 sum += arr[i] 也會(huì)對(duì)應(yīng)一個(gè)存根,它會(huì)把 += 操作作為整數(shù)加法。
然而,我們并不能保證 sum 和 arr[i] 都是整數(shù)。
因?yàn)?JavaScript 中的數(shù)據(jù)類型是動(dòng)態(tài)的,所以在后續(xù)的循環(huán)中,arr[i] 可能就變成了字符串。
而整數(shù)加法和字符串連接是兩種完全不同的操作,所以它們會(huì)被編譯為完全不同的機(jī)器代碼。
對(duì)于這種情況,JIT 的處理方式是編譯成多種不同的基準(zhǔn)存根。
如果每次調(diào)用代碼都使用相同的數(shù)據(jù)類型,那么只會(huì)生成一種存根;如果每次調(diào)用使用不同的數(shù)據(jù)類型,那么會(huì)生成每種類型組合起來的存根。
也就意味著,JIT 在選擇一個(gè)存根之前必須先做好多判斷。
因?yàn)槊恳恍写a在基準(zhǔn)編譯器中都會(huì)有它自己的存根集合,所以每行代碼運(yùn)行時(shí) JIT 需要一直進(jìn)行類型判斷。因此,在該循環(huán)中的每一次遍歷,它都要進(jìn)行相同的類型判斷過程。
如果 JIT 不需要每次都重復(fù)這些類型判斷,那么代碼跑起來就會(huì)更快。而這正是優(yōu)化編譯器所做的優(yōu)化之一。
在優(yōu)化編譯器中,整個(gè)函數(shù)是一起編譯的。所以可以把類型判斷移到循環(huán)之前。
一些 JIT 對(duì)此做了更深的優(yōu)化。比如,在 Firefox 中,我們把只包含整數(shù)的數(shù)組劃分為特殊的數(shù)組分類。如果 arr 是這種數(shù)組,那么 JIT 就不需要檢查 arr[i] 是否是一個(gè)整數(shù)了。這樣的話,JIT 可以在進(jìn)入循環(huán)之前就做完所有的類型判斷。
結(jié)束以上就是對(duì) JIT 的簡(jiǎn)短介紹。
通過監(jiān)視代碼運(yùn)行,編譯熱代碼等方式,JIT 使得 JavaScript 代碼跑的更快。這為大多數(shù) JavaScript 應(yīng)用帶來了許多性能改進(jìn)。
盡管做了這些優(yōu)化,但是 JavaScript 的性能可能仍然無法預(yù)測(cè)。
因?yàn)樽鲞@些優(yōu)化的同時(shí),我們也給運(yùn)行時(shí)增加了額外的開銷,包括:
優(yōu)化和去優(yōu)化過程
監(jiān)視器記錄和恢復(fù)信息占用的內(nèi)存
用于保存基準(zhǔn)、優(yōu)化后函數(shù)的內(nèi)存
不過,對(duì)于這些仍然有改進(jìn)的空間,我們可以消除這些額外開銷,使得性能提升更具可預(yù)測(cè)性。
而這就是 WebAssembly 所做的一件事情!
在下一篇文章中,我們將更詳細(xì)介紹 WebAssembly ,以及它是如跟編譯器一起工作的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/94753.html
摘要:性能簡(jiǎn)史在年,被創(chuàng)造出來時(shí)并不是沖著性能去的。而且在之后的十年發(fā)展中,它的性能一直是很低的。的引入成就了性能提升的一個(gè)轉(zhuǎn)折點(diǎn),其執(zhí)行速度比以往快了之多。性能提升也使得在全新的問題上使用成為可能。現(xiàn)在,極可能是下一個(gè)性能轉(zhuǎn)折點(diǎn)。 你可能已經(jīng)聽說 WebAssembly 代碼跑起來非常快。但是你知道這是為什么嗎?在本系列文章中,我們將探究其原因。 何為 WebAssembly WebAss...
摘要:本文是圖說系列文章的第五篇。這樣的話,使用的開發(fā)者也不需要做任何適配,但是它們卻能獲得更高性能。該圖并不是用來準(zhǔn)確的衡量其性能的。運(yùn)行編寫出高性能的代碼是可能的。這種清理工作由引擎自動(dòng)進(jìn)行,稱為垃圾回收。 本文是圖說 WebAssembly 系列文章的第五篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 在上一篇文章中,我們說到了使用 WebAssembly 和 JavaScript...
摘要:現(xiàn)狀年月日,主流的四大瀏覽器達(dá)成了共識(shí)并宣布的最小可行產(chǎn)品已經(jīng)完成。更快的函數(shù)調(diào)用當(dāng)前,在中調(diào)用函數(shù)比想象的要慢。直接操作目前,沒有任何方式能夠操作。這就導(dǎo)致了部分應(yīng)用可能會(huì)因此而推遲發(fā)布時(shí)間。結(jié)束現(xiàn)如今已經(jīng)相當(dāng)快速。 本文是圖說 WebAssembly 系列文章的最后一篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 現(xiàn)狀 2017 年 2 月 28 日,主流的四大瀏覽器達(dá)成了共識(shí)...
摘要:為了更好的理解,我們有必要去先理解什么是匯編,以及編譯器是如何產(chǎn)生匯編的。什么是匯編現(xiàn)在,我們來看看外星人的大腦是如何工作的。這些注釋就是匯編,也稱為符號(hào)機(jī)器碼。結(jié)束以上的內(nèi)容就是什么是匯編以及它是如何從高級(jí)編程語(yǔ)言翻譯過來的。 本文是圖說 WebAssembly 系列文章的第三篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 為了更好的理解 WebAssembly ,我們有必要去先...
摘要:本文是圖說系列文章的第四篇。它們表示一種可以在普遍流行機(jī)器上高效使用的指令集合。這是因?yàn)槭且环N稱為堆棧機(jī)器。盡管是根據(jù)堆棧機(jī)器來設(shè)計(jì)的,但是這并不是它在真實(shí)物理機(jī)器上工作的方式。這些內(nèi)容稱為段。 本文是圖說 WebAssembly 系列文章的第四篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 WebAssembly 是一種使得除 JavaScript 以外的編程語(yǔ)言也能運(yùn)行在網(wǎng)頁(yè)上...
閱讀 3226·2021-11-02 14:44
閱讀 3729·2021-09-02 15:41
閱讀 1672·2019-08-29 16:57
閱讀 1793·2019-08-26 13:38
閱讀 3302·2019-08-23 18:13
閱讀 2112·2019-08-23 15:41
閱讀 1677·2019-08-23 14:24
閱讀 3035·2019-08-23 14:03