WebAssembly 系列(三)編譯器如何生成彙編

鬍子大哈發表於2017-03-12

作者:Lin Clark

編譯:鬍子大哈

翻譯原文:huziketang.com/blog/posts/…

英文原文:A crash course in just-in-time (JIT) compilers

轉載請註明出處,保留原文連結以及作者資訊


本文是關於 WebAssembly 系列的第二篇文章。如果你沒有讀先前文章的話,建議先讀這裡。如果對 WebAssembly 沒概念,建議先讀這裡(中文文章)

JavaScript 的啟動比較緩慢,但是通過 JIT 可以使其變快,那麼 JIT 是如何起作用的呢?

JavaScript 在瀏覽器中是如何執行的?

如果是你一個開發者,當你決定在你的頁面中使用 JavaScript 的時候,有兩個要考慮的事情:目標和問題。

目標:告訴計算機你想做什麼。

問題:你和計算機說不同的語言,無法溝通。

你說的是人類的語言,而計算機用的是機器語言。機器語言也是一種語言,只是 JavaScript 或者其他高階程式語言機器能看得懂,而人類不用他們來交流罷了。它們是基於人類認知而設計出來的。

所以呢,JavaScript 引擎的工作就是把人類的語言轉換成機器能看懂的語言。

這就像電影《降臨》中,人類和外星人的互相交流一樣。

WebAssembly 系列(三)編譯器如何生成彙編

在電影裡面,人類和外星人不僅僅是語言不同,兩個群體看待世界的方式都是不一樣的。其實人類和機器也是類似(後面我會詳細介紹)。

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

在程式碼的世界中,通常有兩種方式來翻譯機器語言:直譯器和編譯器。

如果是通過直譯器,翻譯是一行行地邊解釋邊執行

WebAssembly 系列(三)編譯器如何生成彙編

編譯器是把原始碼整個編譯成目的碼,執行時不再需要編譯器,直接在支援目的碼的平臺上執行。

WebAssembly 系列(三)編譯器如何生成彙編

這兩種翻譯的方式都各有利弊。

直譯器的利弊

直譯器啟動和執行的更快。你不需要等待整個編譯過程完成就可以執行你的程式碼。從第一行開始翻譯,就可以依次繼續執行了。

正是因為這個原因,直譯器看起來更加適合 JavaScript。對於一個 Web 開發人員來講,能夠快速執行程式碼並看到結果是非常重要的。

這就是為什麼最開始的瀏覽器都是用 JavaScript 直譯器的原因。

可是當你執行同樣的程式碼一次以上的時候,直譯器的弊處就顯現出來了。比如你執行一個迴圈,那直譯器就不得不一次又一次的進行翻譯,這是一種效率低下的表現。

編譯器的利弊

編譯器的問題則恰好相反。

它需要花一些時間對整個原始碼進行編譯,然後生成目標檔案才能在機器上執行。對於有迴圈的程式碼執行的很快,因為它不需要重複的去翻譯每一次迴圈。

另外一個不同是,編譯器可以用更多的時間對程式碼進行優化,以使的程式碼執行的更快。而直譯器是在 runtime 時進行這一步驟的,這就決定了它不可能在翻譯的時候用很多時間進行優化。

Just-in-time 編譯器:綜合了兩者的優點

為了解決直譯器的低效問題,後來的瀏覽器把編譯器也引入進來,形成混合模式。

不同的瀏覽器實現這一功能的方式不同,不過其基本思想是一致的。在 JavaScript 引擎中增加一個監視器(也叫分析器)。監視器監控著程式碼的執行情況,記錄程式碼一共執行了多少次、如何執行的等資訊。

起初,監視器監視著所有通過直譯器的程式碼。

WebAssembly 系列(三)編譯器如何生成彙編

如果同一行程式碼執行了幾次,這個程式碼段就被標記成了 “warm”,如果執行了很多次,則被標記成 “hot”。

基線編譯器

如果一段程式碼變成了 “warm”,那麼 JIT 就把它送到編譯器去編譯,並且把編譯結果儲存起來。

WebAssembly 系列(三)編譯器如何生成彙編

程式碼段的每一行都會被編譯成一個“樁”(stub),同時給這個樁分配一個以“行號 + 變數型別”的索引。如果監視器監視到了執行同樣的程式碼和同樣的變數型別,那麼就直接把這個已編譯的版本 push 出來給瀏覽器。

通過這樣的做法可以加快執行速度,但是正如前面我所說的,編譯器還可以找到更有效地執行程式碼的方法,也就是做優化。

基線編譯器可以做一部分這樣的優化(下面我會給出例子),不過基線編譯器優化的時間不能太久,因為會使得程式的執行在這裡 hold 住。

不過如果程式碼確實非常 “hot”(也就是說幾乎所有的執行時間都耗費在這裡),那麼花點時間做優化也是值得的。

優化編譯器

如果一個程式碼段變得 “very hot”,監視器會把它傳送到優化編譯器中。生成一個更快速和高效的程式碼版本出來,並且儲存之。

WebAssembly 系列(三)編譯器如何生成彙編

為了生成一個更快速的程式碼版本,優化編譯器必須做一些假設。例如,它會假設由同一個建構函式生成的例項都有相同的形狀——就是說所有的例項都有相同的屬性名,並且都以同樣的順序初始化,那麼就可以針對這一模式進行優化。

整個優化器起作用的鏈條是這樣的,監視器從他所監視程式碼的執行情況做出自己的判斷,接下來把它所整理的資訊傳遞給優化器進行優化。如果某個迴圈中先前每次迭代的物件都有相同的形狀,那麼就可以認為它以後迭代的物件的形狀都是相同的。可是對於 JavaScript 從來就沒有保證這麼一說,前 99 個物件保持著形狀,可能第 100 個就少了某個屬性。

正是由於這樣的情況,所以編譯程式碼需要在執行之前檢查其假設是不是合理的。如果合理,那麼優化的編譯程式碼會執行,如果不合理,那麼 JIT 會認為做了一個錯誤的假設,並且把優化程式碼丟掉。

WebAssembly 系列(三)編譯器如何生成彙編

這時(發生優化程式碼丟棄的情況)執行過程將會回到直譯器或者基線編譯器,這一過程叫做去優化

通常優化編譯器會使得程式碼變得更快,但是一些情況也會引起一些意想不到的效能問題。如果你的程式碼一直陷入優化<->去優化的怪圈,那麼程式執行將會變慢,還不如基線編譯器快。

大多數的瀏覽器都做了限制,當優化/去優化迴圈發生的時候會嘗試跳出這種迴圈。比如,如果 JIT 做了 10 次以上的優化並且又丟棄的操作,那麼就不繼續嘗試去優化這段程式碼了樁。

一個優化的例子:型別特化(Type specialization)

有很多不同型別的優化方法,這裡我介紹一種,讓大家能夠明白是如何優化的。優化編譯器最成功一個特點叫做型別特化,下面詳細解釋。

JavaScript 所使用的動態型別體系在執行時需要進行額外的解釋工作,例如下面程式碼:

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

+= 迴圈中這一步看起來很簡單,只需要進行一步計算,但是恰恰因為是用動態型別,他所需要的步驟要比你所想象的更復雜一些。

我們假設 arr 是一個有 100 個整數的陣列。當程式碼被標記為 “warm” 時,基線編譯器就為函式中的每一個操作生成一個樁。sum += arr[i] 會有一個相應的樁,並且把裡面的 += 操作當成整數加法。

但是,sumarr[i] 兩個數並不保證都是整數。因為在 JavaScript 中型別都是動態型別,在接下來的迴圈當中,arr[i] 很有可能變成了 string 型別。整數加法和字串連線是完全不同的兩個操作,會被編譯成不同的機器碼。

JIT 處理這個問題的方法是編譯多基線樁。如果一個程式碼段是單一形態的(即總是以同一型別被呼叫),則只生成一個樁。如果是多形態的(即呼叫的過程中,型別不斷變化),則會為操作所呼叫的每一個型別組合生成一個樁。

這就是說 JIT 在選擇一個樁之前,會進行多分枝選擇,類似於決策樹,問自己很多問題才會確定最終選擇哪個,見下圖:

WebAssembly 系列(三)編譯器如何生成彙編

正是因為在基線編譯器中每行程式碼都有自己的樁,所以 JIT 在每行程式碼被執行的時候都會檢查資料型別。在迴圈的每次迭代,JIT 也都會重複一次分枝選擇。

WebAssembly 系列(三)編譯器如何生成彙編

如果程式碼在執行的過程中,JIT 不是每次都重複檢查的話,那麼執行的還會更快一些,而這就是優化編譯器所需要做的工作之一了。

優化編譯器中,整個函式被統一編譯,這樣的話就可以在迴圈開始執行之前進行型別檢查。

WebAssembly 系列(三)編譯器如何生成彙編

一些瀏覽器的 JIT 優化更加複雜。比如在 Firefox 中,給一些陣列設定了特定的型別,比如裡面只包含整型。如果 arr 是這種陣列型別,那麼 JIT 就不需要檢查 arr[i] 是不是整型了,這也意味著 JIT 可以在進入迴圈之前進行所有的型別檢查。

總結

簡而言之 JIT 是什麼呢?它是使 JavaScript 執行更快的一種手段,通過監視程式碼的執行狀態,把 hot 程式碼(重複執行多次的程式碼)進行優化。通過這種方式,可以使 JavaScript 應用的效能提升很多倍。

為了使執行速度變快,JIT 會增加很多多餘的開銷,這些開銷包括:

  • 優化和去優化開銷
  • 監視器記錄資訊對記憶體的開銷
  • 發生去優化情況時恢復資訊的記錄對記憶體的開銷
  • 對基線版本和優化後版本記錄的記憶體開銷

這裡還有很大的提升空間:即消除開銷。通過消除開銷使得效能上有進一步地提升,這也是 WebAssembly 所要做的事之一。


我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點

相關文章