摘要:解釋器的利弊解釋器啟動和執行的更快。正是因為這個原因,解釋器看起來更加適合。這就是為什么最開始的瀏覽器都是用解釋器的原因。可是當你運行同樣的代碼一次以上的時候,解釋器的弊處就顯現出來了。起初,監視器監視著所有通過解釋器的代碼。
作者:Lin Clark
編譯:胡子大哈
翻譯原文:http://huziketang.com/blog/posts/detail?postId=58c12f36a6d8a07e449fdd22
英文原文:A crash course in just-in-time (JIT) compilers
轉載請注明出處,保留原文鏈接以及作者信息
本文是關于 WebAssembly 系列的第二篇文章。如果你沒有讀先前文章的話,建議先讀這里。如果對 WebAssembly 沒概念,建議先讀這里(中文文章)。
JavaScript 的啟動比較緩慢,但是通過 JIT 可以使其變快,那么 JIT 是如何起作用的呢?
JavaScript 在瀏覽器中是如何運行的?如果是你一個開發者,當你決定在你的頁面中使用 JavaScript 的時候,有兩個要考慮的事情:目標和問題。
目標:告訴計算機你想做什么。
問題:你和計算機說不同的語言,無法溝通。
你說的是人類的語言,而計算機用的是機器語言。機器語言也是一種語言,只是 JavaScript 或者其他高級編程語言機器能看得懂,而人類不用他們來交流罷了。它們是基于人類認知而設計出來的。
所以呢,JavaScript 引擎的工作就是把人類的語言轉換成機器能看懂的語言。
這就像電影《降臨》中,人類和外星人的互相交流一樣。
在電影里面,人類和外星人不僅僅是語言不同,兩個群體看待世界的方式都是不一樣的。其實人類和機器也是類似(后面我會詳細介紹)。
那么翻譯是如何進行的呢?
在代碼的世界中,通常有兩種方式來翻譯機器語言:解釋器和編譯器。
如果是通過解釋器,翻譯是一行行地邊解釋邊執行
編譯器是把源代碼整個編譯成目標代碼,執行時不再需要編譯器,直接在支持目標代碼的平臺上運行。
這兩種翻譯的方式都各有利弊。
解釋器的利弊解釋器啟動和執行的更快。你不需要等待整個編譯過程完成就可以運行你的代碼。從第一行開始翻譯,就可以依次繼續執行了。
正是因為這個原因,解釋器看起來更加適合 JavaScript。對于一個 Web 開發人員來講,能夠快速執行代碼并看到結果是非常重要的。
這就是為什么最開始的瀏覽器都是用 JavaScript 解釋器的原因。
可是當你運行同樣的代碼一次以上的時候,解釋器的弊處就顯現出來了。比如你執行一個循環,那解釋器就不得不一次又一次的進行翻譯,這是一種效率低下的表現。
編譯器的利弊編譯器的問題則恰好相反。
它需要花一些時間對整個源代碼進行編譯,然后生成目標文件才能在機器上執行。對于有循環的代碼執行的很快,因為它不需要重復的去翻譯每一次循環。
另外一個不同是,編譯器可以用更多的時間對代碼進行優化,以使的代碼執行的更快。而解釋器是在 runtime 時進行這一步驟的,這就決定了它不可能在翻譯的時候用很多時間進行優化。
Just-in-time 編譯器:綜合了兩者的優點為了解決解釋器的低效問題,后來的瀏覽器把編譯器也引入進來,形成混合模式。
不同的瀏覽器實現這一功能的方式不同,不過其基本思想是一致的。在 JavaScript 引擎中增加一個監視器(也叫分析器)。監視器監控著代碼的運行情況,記錄代碼一共運行了多少次、如何運行的等信息。
起初,監視器監視著所有通過解釋器的代碼。
如果同一行代碼運行了幾次,這個代碼段就被標記成了 “warm”,如果運行了很多次,則被標記成 “hot”。
基線編譯器如果一段代碼變成了 “warm”,那么 JIT 就把它送到編譯器去編譯,并且把編譯結果存儲起來。
代碼段的每一行都會被編譯成一個“樁”(stub),同時給這個樁分配一個以“行號 + 變量類型”的索引。如果監視器監視到了執行同樣的代碼和同樣的變量類型,那么就直接把這個已編譯的版本 push 出來給瀏覽器。
通過這樣的做法可以加快執行速度,但是正如前面我所說的,編譯器還可以找到更有效地執行代碼的方法,也就是做優化。
基線編譯器可以做一部分這樣的優化(下面我會給出例子),不過基線編譯器優化的時間不能太久,因為會使得程序的執行在這里 hold 住。
不過如果代碼確實非常 “hot”(也就是說幾乎所有的執行時間都耗費在這里),那么花點時間做優化也是值得的。
優化編譯器如果一個代碼段變得 “very hot”,監視器會把它發送到優化編譯器中。生成一個更快速和高效的代碼版本出來,并且存儲之。
為了生成一個更快速的代碼版本,優化編譯器必須做一些假設。例如,它會假設由同一個構造函數生成的實例都有相同的形狀——就是說所有的實例都有相同的屬性名,并且都以同樣的順序初始化,那么就可以針對這一模式進行優化。
整個優化器起作用的鏈條是這樣的,監視器從他所監視代碼的執行情況做出自己的判斷,接下來把它所整理的信息傳遞給優化器進行優化。如果某個循環中先前每次迭代的對象都有相同的形狀,那么就可以認為它以后迭代的對象的形狀都是相同的。可是對于 JavaScript 從來就沒有保證這么一說,前 99 個對象保持著形狀,可能第 100 個就少了某個屬性。
正是由于這樣的情況,所以編譯代碼需要在運行之前檢查其假設是不是合理的。如果合理,那么優化的編譯代碼會運行,如果不合理,那么 JIT 會認為做了一個錯誤的假設,并且把優化代碼丟掉。
這時(發生優化代碼丟棄的情況)執行過程將會回到解釋器或者基線編譯器,這一過程叫做去優化。
通常優化編譯器會使得代碼變得更快,但是一些情況也會引起一些意想不到的性能問題。如果你的代碼一直陷入優化<->去優化的怪圈,那么程序執行將會變慢,還不如基線編譯器快。
大多數的瀏覽器都做了限制,當優化/去優化循環發生的時候會嘗試跳出這種循環。比如,如果 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] 會有一個相應的樁,并且把里面的 += 操作當成整數加法。
但是,sum 和 arr[i] 兩個數并不保證都是整數。因為在 JavaScript 中類型都是動態類型,在接下來的循環當中,arr[i] 很有可能變成了 string 類型。整數加法和字符串連接是完全不同的兩個操作,會被編譯成不同的機器碼。
JIT 處理這個問題的方法是編譯多基線樁。如果一個代碼段是單一形態的(即總是以同一類型被調用),則只生成一個樁。如果是多形態的(即調用的過程中,類型不斷變化),則會為操作所調用的每一個類型組合生成一個樁。
這就是說 JIT 在選擇一個樁之前,會進行多分枝選擇,類似于決策樹,問自己很多問題才會確定最終選擇哪個,見下圖:
正是因為在基線編譯器中每行代碼都有自己的樁,所以 JIT 在每行代碼被執行的時候都會檢查數據類型。在循環的每次迭代,JIT 也都會重復一次分枝選擇。
如果代碼在執行的過程中,JIT 不是每次都重復檢查的話,那么執行的還會更快一些,而這就是優化編譯器所需要做的工作之一了。
優化編譯器中,整個函數被統一編譯,這樣的話就可以在循環開始執行之前進行類型檢查。
一些瀏覽器的 JIT 優化更加復雜。比如在 Firefox 中,給一些數組設定了特定的類型,比如里面只包含整型。如果 arr 是這種數組類型,那么 JIT 就不需要檢查 arr[i] 是不是整型了,這也意味著 JIT 可以在進入循環之前進行所有的類型檢查。
總結簡而言之 JIT 是什么呢?它是使 JavaScript 運行更快的一種手段,通過監視代碼的運行狀態,把 hot 代碼(重復執行多次的代碼)進行優化。通過這種方式,可以使 JavaScript 應用的性能提升很多倍。
為了使執行速度變快,JIT 會增加很多多余的開銷,這些開銷包括:
優化和去優化開銷
監視器記錄信息對內存的開銷
發生去優化情況時恢復信息的記錄對內存的開銷
對基線版本和優化后版本記錄的內存開銷
這里還有很大的提升空間:即消除開銷。通過消除開銷使得性能上有進一步地提升,這也是 WebAssembly 所要做的事之一。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86853.html
摘要:但是為什么執行的更快呢在這個系列文章中,我會為你解釋這一點。所以當人們說更快的時候,一般來講是與相比而言的。被人們廣為傳播的性能大戰在年打響。性能的提升使得的應用范圍得到很大的擴展。現在通過,我們很有可能正處于第二個拐點。 作者:Lin Clark 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58ce8036...
摘要:性能簡史在年,被創造出來時并不是沖著性能去的。而且在之后的十年發展中,它的性能一直是很低的。的引入成就了性能提升的一個轉折點,其執行速度比以往快了之多。性能提升也使得在全新的問題上使用成為可能。現在,極可能是下一個性能轉折點。 你可能已經聽說 WebAssembly 代碼跑起來非常快。但是你知道這是為什么嗎?在本系列文章中,我們將探究其原因。 何為 WebAssembly WebAss...
摘要:解釋器的利弊解釋器啟動和執行的更快。正是因為這個原因,解釋器看起來更加適合。這就是為什么最開始的瀏覽器都是用解釋器的原因。可是當你運行同樣的代碼一次以上的時候,解釋器的弊處就顯現出來了。起初,監視器監視著所有通過解釋器的代碼。 作者:Lin Clark 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58c12f...
摘要:解釋器的利弊解釋器啟動和執行的更快。正是因為這個原因,解釋器看起來更加適合。這就是為什么最開始的瀏覽器都是用解釋器的原因。可是當你運行同樣的代碼一次以上的時候,解釋器的弊處就顯現出來了。起初,監視器監視著所有通過解釋器的代碼。 作者:Lin Clark 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58c12f...
摘要:并且于年月日,四個主要的瀏覽器一致同意宣布的版本已經完成,即將推出一個瀏覽器可以搭載的穩定版本。因此本文著重介紹為什么比更快。本文主要表達的是為什么應該是更快的。則不同,它是由幾大主要的瀏覽器廠商共同設計的。 作者:Alon Zakai 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58ce80d2a6d8a0...
閱讀 2655·2023-04-26 02:44
閱讀 8253·2021-11-22 14:44
閱讀 2119·2021-09-27 13:36
閱讀 2463·2021-09-08 10:43
閱讀 676·2019-08-30 15:56
閱讀 1392·2019-08-30 15:55
閱讀 2887·2019-08-28 18:12
閱讀 2826·2019-08-26 13:50