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