[譯][A crash course in WebAssembly] Just-in-time(JIT)編譯器速成課

Yiniau發表於2019-02-27

序言

譯者大三,大一下在工作室前輩影響下入門前端,深度認可這個方向,一去不復返。奈何水平有限。

做翻譯並不是為了造福大眾,而只是方便自己看,順便總結,向志同道合者分享自己認為有趣的東西。

譯者英語水平不高,譯文九成翻譯一成總結,如果十分在意可能存在的表述偏差,自行點選連結探訪原文。

最後,歡迎指正和討論。

Just-in-time(JIT)編譯器速成課

@source: 原文地址

@auther: Lin Clark

@tags: WebAssembly, javascript, performence


這是「是什麼讓WebAssembly這麼快」系列的第二部分。如果您還沒有閱讀其他的,我們建議從頭開始


Javascript是如何在瀏覽器中執行的

當你作為一個開發人員向一個頁面中新增Javascript時,你會有一個目標和一個問題。

Goal:你想告訴計算機該做什麼 Problem:你和電腦說的不是一種語言

你說的是人類語言,而電腦說機器語言。就像你不把JavaScript或者其他高階程式語言當成人類語言,他們也是如此。即便已經被設計為人類的認知,程式語言依舊無法被機器認知。

所以JavaScript引擎的工作就是把人類語言變成機器可以理解的東西。

我覺得這個就像電影Arriva,那裡有人類和外星人在試圖互相交談。

example image

在那部電影裡,人類和外星人不只是逐字翻譯。這兩個群體對世界有著不同的思考方式。人類和機器也是如此(我將在下一篇文章中詳細解釋這一點)。

那麼翻譯是如何進行的呢?

在程式世界中,通常有兩種翻譯成機器語言的途徑。即使用 直譯器(interpreter)編譯器(compiler)

其中直譯器逐行翻譯,類似聽譯

interpreter

編譯器則一口氣完成所有翻譯,並記錄在新的檔案中。

compiler

處理翻譯的每種方式都有優點和缺點

直譯器的優點和缺點

直譯器可以快速啟動並執行。在開始執行程式碼之前,您不必經歷整個編譯步驟。只需要開始翻譯第一行並執行它。

正因為如此,直譯器似乎直譯器天生適合Javascript這樣的語言。 Web開發人員能夠快速開始並執行程式碼是非常重要的。

這就是瀏覽器開始使用JavaScript直譯器的原因。

但是當你不止一次執行相同的程式碼時,使用直譯器的缺點就來了。想象一下有一個迴圈,那麼直譯器勢必一遍又一遍地做同樣的翻譯。

編譯器的優缺點

編譯器有相反的權衡。

開始時必須經過編譯步驟,啟動就需要更多一點的時間,但是,迴圈中的程式碼不需要在每個迴圈中重複翻譯,得以執行得更快。

另一個區別是,編譯器有更多的時間來檢視程式碼並對其進行解析,從而獲得更好的效能。這樣的處理被稱為優化(optimizations)。

相對的直譯器在執行時(runtime)做它的工作,所以在翻譯階段不會有太多的時間來找出這些優化。

Just-in-time 編譯器: 兩全其美

作為一種擺脫直譯器每次瀏覽迴圈時必須不斷翻譯程式碼的方法————瀏覽器開始將編譯器混合進來。

不同的瀏覽器以稍微不同的方式來做這件事,但基本的思路是一樣的。他們在JavaScript引擎中增加了一個新的部分,叫做監視器(a monitor, 也稱 profiler)。該監視器在執行時(runtime)監視程式碼,並記錄執行的次數以及使用的型別。

起初,監視器只是看著直譯器執行。

monitor

如果同一行程式碼重複執行幾次,那麼這段程式碼被稱為溫暖的(warm)。如果它執行了非常多次,那就稱為熱門(hot)。

baseline 編譯器

當一個函式開始變暖時,JIT將把它傳送出去編譯,然後儲存該編譯。

getting warm

函式的每一行都被編譯成一個“存根(stub)”。stub以行號和變數型別為索引(我會解釋為什麼這很重要)。如果監視器看到execution是使用相同的變數型別再次觸發相同的程式碼,那麼它將只提取其編譯的版本。

這有助於加快速度。但正如我所說,編譯器可以做更多。但要找出最高效的方法會需要更多的時間..來進行優化。

baseline編譯器將進行一些優化(我給出了一個下面的例子)。它不想花太多時間,也因為它不想要太長的執行時間。

但是,如果程式碼真的很熱(hot)————如果它執行了很多次,那麼值得花費額外的時間來進行更多的優化。

Optimizing 編譯器

當部分程式碼變熱(hot)時,監視器將把它傳送給優化編譯器。這將建立另一個更快的函式,這個版本也將被儲存。

Optimizing compiler

為了製作更快的程式碼版本,優化編譯器必須做出一些假設(assumptions)。

例如,如果它可以假設由特定建構函式建立的所有物件具有相同的形狀(shape),即它們總是具有相同的屬性名稱,並且這些屬性是以相同的順序新增的 ———— 然後可以根據這些屬性切割一些角落。

For example, if it can assume that all objects created by a particular constructor have the same shape—that is, that they always have the same property names, and that those properties were added in the same order— then it can cut some corners based on that.

優化編譯器使用監視器通過監視程式碼執行來進行判斷產生的資訊。如果以前所有的迴圈都是事實,那麼它就會認為假設將繼續是真實的(be true)。

但是,對於JavaScript,當然也沒有任何保證(...型別其實挺重要的)。你可以有99個objects都具有相同的shape,但是第100個Object可能缺少一個屬性。

所以編譯的程式碼在執行之前需要檢查,看看這些假設是否有效。如果是,則執行編譯的程式碼。但如果不是,JIT假定自己做出了錯誤的假設,並且破壞了(trashes)優化的程式碼。

deoptimization

然後執行回到直譯器或baseline編譯版本。這個過程被稱為去優化(deoptimization, or bailing out)。

說句題外話,阿里有個面試官面我的時候說TypeScript能不能改善js的效能,我回答的是沒辦法從根本上改變,不過ts有型別系統,我有使用Flow type的經驗,就回答了可能會減少去優化所造成的效能浪費,也不知道對不對,有懂的小夥伴求指點迷津

通常優化編譯器會使程式碼更快,但是有時它們可能會導致意外的效能問題。如果你的程式碼不斷優化,然後取消優化,那麼最終的執行速度比只執行基準編譯版本要慢。

大多數瀏覽器增加了限制,以突破優化/去優化迴圈。如果JIT做了10次以上的優化嘗試,並且不斷丟擲,那麼就會停止嘗試。

一個優化的例子: Type specialization

優化有很多不同的型別,但我想舉例一種型別,讓你們可以感覺到優化如何發生。在優化編譯器方面最大的勝利之一來自型別專業化(Type specialization)。

JavaScript使用的動態型別系統在執行時需要一些額外的工作。例如以下程式碼:

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}
複製程式碼

迴圈中的 += 步驟可能看起來很簡單。看起來好你可以一步計算出來,但是由於動態型別(dynamic type),它需要比預期更多的步驟。

我們假設 arr 是一個100個整數的陣列。一旦程式碼變暖,基線編譯器將為函式中的每個操作建立一個stub。所以會有一個stub sum + = arr [i] ,它將 += 操作作為整數加法處理。

JIT處理這個問題的方式是編譯多個 baseline stubs 。如果一段程式碼是單形的(即總是用相同的型別呼叫),它將得到一個stub。如果它是多型的(從一次通過程式碼到另一次通過不同的型別呼叫),那麼它將獲得通過該操作的每個型別組合的存根。

這意味著JIT在選擇存根之前必須提出許多問題。

judge type

由於每行程式碼在baseline編譯器中都有自己的一組stub,所以每次執行程式碼時,JIT都需要繼續檢查這些型別。因此,對於迴圈中的每一次迭代,都必須提出相同的問題。

iteration

如果JIT不需要重複這些檢查,程式碼將會執行得更快。這是優化編譯器所做的事情之一。

在優化編譯器中,整個函式被編譯在一起。型別檢查被移動,以便它們在迴圈之前發生。

optimized iteration

一些JIT甚至進一步優化。例如,在Firefox中,對於只包含整數的陣列有一個特殊的分類。如果arr是這些陣列之一,那麼JIT不需要檢查 arr[i] 是否是一個整數。這意味著JIT可以在進入迴圈之前完成所有的型別檢查。

總結

簡而言之,這就是JIT。它通過監視執行它的程式碼併傳送熱程式碼路徑進行優化,使JavaScript執行得更快。使大多數JavaScript應用程式效能的大幅提升。

但即使有了這些改進,JavaScript的效能依舊是不可預測的。為了加快速度,JIT在執行時增加了一些開銷,包括:

  • optimization and deoptimization(優化和去優化)
  • memory used for the monitor’s bookkeeping and recovery information for when bailouts happen(儲存在deoptimation發生時需要的監視器的簿記和恢復資訊)
  • memory used to store baseline and optimized versions of a function(儲存baseline版本的函式和優化後的函式)

這裡還有改進的空間:可以消除開銷,使效能更可預測。這也是WebAssembly所做的一件事情。

下一篇文章中,我將解釋更多關於彙編以及編譯器如何使用它的知識。

推一波我的部落格

相關文章