經過上一篇文章,咱們知道了JavaScript引擎是執行JavaScript代碼的程序或解釋器,瞭解了JavaScript引擎的基本工做原理。咱們常常據說的JavaScript引擎就是V8引擎,這篇文章咱們就來認識一下V8引擎,咱們先來看一下除了V8引擎,還有哪些JS引擎:javascript
Microsoft Edge
在這些項目中,V8引擎因其在性能上的突出表現,倍受你們的關注,因此咱們也以介紹V8引擎爲主。V8是Google開源的高性能JavaScript引擎,用C++編寫。它用於谷歌瀏覽器,谷歌的開源瀏覽器,以及Node.js等等。java
速度是V8追求的主要設計目標之一,在一些性能測試中,V8比IE的JScript,Firefox中的SpiderMonkey以及Safari中的JavaScriptCore要快上數倍。相比其餘的JavaScript引擎轉化成字節碼或解釋執行,V8將其編譯成本地代碼,而且使用瞭如隱類型,內聯緩存等方法來提升性能。node
http://kourge.net/node/122linux
V8按照ECMA-262第5版中的規定實施ECMAScript,支持衆多操做系統,如windows、linux、android等,也支持其餘硬件架構,如IA32,X64,ARM等,具備很好的可移植和跨平臺特性。android
V8工做的整個過程與Java有些相似,大體分紅兩個階段:第一是編譯,第二是運行。與C++直接編譯成本地代碼不一樣的是,V8只有在函數調用時纔會編譯成本地代碼,這樣就提升了響應時間減小了時間開銷。git
圖片來源《WebKit技術內幕》github
在V8引擎中,源代碼先經過解析器轉變成抽象語法樹,這點同JavaScriptCore引擎同樣,不一樣於JavaScriptCore引擎,V8引擎中並不將抽象語法樹轉變成字節碼或者其餘中間表示,而是經過JIT全代碼生成器(full code generator)從抽象語法樹直接生成本地代碼,這樣作能夠減小抽象語法樹到字節碼的轉換時間,提升代碼的執行速度,但也是由於缺乏了轉換爲字節碼這一中間過程,也就減小了優化中間代碼的機會。web
下面來看一下V8引擎編譯JavaScript生成本地代碼使用了哪些主要類:正則表達式
圖片來源《WebKit技術內幕》
JavaScript代碼編譯的過程大體爲:Script類調用Compiler類的Compile函數生成本地代碼。在該函數中,先使用Parser類來生成抽象語法樹;再使用FullCodeGenerator類來生成本地代碼。
圖片來源《WebKit技術內幕》
本地代碼與具體的硬件平臺密切相關,FullCodeGenerator使用多個後端來生成與平臺相匹配的本地彙編代碼。因爲FullCodeGenerator經過遍歷AST來爲每一個節點生成相應的彙編代碼,缺失了全局視圖,節點之間的優化也就無從談起。
JavaScript代碼編譯以前須要構建一個運行環境,因此JavaScript代碼編譯以前,V8引擎會構建衆多對象並加載一些內置的庫(如Math庫)。再次強調一下,在JavaScript源碼中,並不是全部的函數都被編譯生成本地代碼,而是延時編譯,在調用時纔會編譯。
因爲V8缺乏生成字節碼(中間表示)這一環節,缺乏必要的優化,爲了性能上的考慮,V8會在生成本地代碼後,使用數據分析器(Profiler)採集一些信息,而後根據這些信息對本地代碼進行優化,生成更高效率的本地代碼,這是一個逐步改進的過程。同時,當發現優化後的代碼性能並無提升甚至還有所下降時,V8將退回到原來的代碼。這些都是在運行階段用涉及到的技術。
如今咱們來看一下運行階段使用到的類:
圖片來源《WebKit技術內幕》
首先,當某個JavaScript函數被調用時,使用編譯階段的類和操做編譯生成本地代碼。具體的工做方式是V8查找函數是否已經生成本地代碼,若是已經生成,那麼直接使用這個函數。不然,V8引擎會觸發生成本地代碼,這樣的工做方式能夠節約時間,減小去處理那些使用不到的代碼的時間。其次,執行編譯後的代碼爲JavaScript構建JS對象,這須要Runtime類來輔助建立對象,並須要從Heap類分配內容。再次,藉助Runtime類中的輔助函數來完成一些功能,如屬性訪問,類型轉換等。最後,將不用的空間進行標記清除和垃圾回收。
圖片來源《WebKit技術內幕》
FullCodeGenerator編譯器基於抽象語法樹直接生成本地代碼,沒有中間表示層,因此不少時候沒有通過很好的優化。JavaScript引擎性能之爭很是激烈,沒有通過優化的代碼致使該引擎在性能上同有特別大的突破,而其餘引擎都在進度,有鑑於此,在2010年,V8引入了新的編譯器,這就是Crankshaft編譯器,它主要針對那些熱點函數進行優化。該編譯器基於JavaScript源代碼開始分析,而不是本地代碼,同時構建Hydtogen圖並基於此來進行優化分析。
FullCodeGenerator是一個簡單且快的編譯器,生成未優化的本地代碼,運行起來很慢;Crankshaft是一個相對慢的編譯器,生成高度優化的代碼。由FullCodeGenerator生成的未優化代碼Crankshaft優化代碼替換,傳送門。
Crankshaft編譯器爲了性能考慮,一般會作出比較樂觀和大膽的預測,那就是編譯器認爲這些代碼比較穩定,變量類型不會發生改變,因此可以生成高效的本地代碼。可是在實際執行過程當中,由於JavaScript弱類型語言的特性,變量類型有可能會改變,在這種狀況下,V8會將該編譯器作的錯誤優化回滾到以前的通常狀況,這個過程稱爲優化回滾。
V8並不僅是第一次執行一個JavaScript函數時才編譯它;同一個JavaScript函數能夠被這些JIT編譯器屢次編譯。
基本流程是:
[JavaScript函數] -> 第一次被調用 -> Full Code -> [初級編譯後的代碼] 足夠熱以後 -> Crankshaft(Optimizing Compiler) -> [優化編譯後的代碼] 若是優化的代碼須要去優化(優化回滾) -> deoptimize -> 回到[初級編譯後的代碼] ... 周而復始 ...
示例以下:
var counter = 0; function test(x,y){ counter ++; if(counter < 10000000){ // do something return 123; } var unknown = new Date(); console.log(unknown); }
函數test被調用屢次後,V8引擎可能會觸發Crankshaft編譯器來生成優化的代碼,優化的代碼認爲示例代碼的類型等信息都已經被獲知,但事實上還未真正執行到new Date()這個地方,並未獲取unknown這個變量的類型,V8只得將該部分的代碼進行回滾。優化回滾是一個很費時的操做,因此在寫代碼的過程當中,儘可能不要觸發這個過程。
咱們都知道JavaScript屬於動態類型語言,只有在運行時才能肯定變量的類型,在運行時計算和決定類型,會帶來嚴重的性能損失,這也就致使了JavaScript語言的運行效率比C++或Java都要低不少。
主要體如今如下幾個部分:
偏移信息共享:
C++屬於靜態類型語言,不能在執行時動態改變類型,這些對象都是共享偏移信息的。訪問對象時就按編譯時的偏移量便可;JavaScript每一個對象都是自描述,屬性和位置偏移信息都包含在自身的結構中。
一個簡單的C++函數:
class Class1 { int x; int y; } int add(Class1 a, Class1 b){ return a.x*a.y + b.x*b.y; }
示例代碼中的類型和對象的結構表示,以下圖:
圖片來源《WebKit技術內幕》
一個簡單的JavaScript函數:
function add(a,b){ return a.x*a.y + b.x*b.y; // 這裏對象a和b的類型未知 } var a = {x:3.3,y:5.5}; var b = {x:4.4,y:6.6};
示例代碼中對象a和b的結構表示,以下圖:
圖片來源《WebKit技術內幕》
由於對象屬性的訪問很是廣泛並且次數很是頻繁,像C++這種經過偏移量來訪問值使用少數兩個彙編指定就能完成,可是Javascript這種經過屬性名來匹配對於性能形成的影響可能會多不少倍,由於屬性名匹配須要特別長的時間,並且額外浪費不少內存空間。
有方法解決這一問題麼?答案是確定的。下面咱們就來看一下V8引擎是如何解決這一問題的。雖然JavaScript語言沒有類型的定義,可是V8使用類和偏移位置思想,將原本須要經過字符串匹配來查找屬性值的算法改進爲使用相似C++編譯器的偏移位置的機制來實現。這就是隱藏類(Hidden Class)。
JavaScript對象的實如今V8中包含3個成員,第一個是隱藏類的指針,這是V8爲JavaScript對象建立的隱藏類。第二個指向這個對象包含的屬性值。第三個指向這個對象包含的元素。
圖片來源《WebKit技術內幕》
隱藏類將對象劃分紅不一樣的組,對於相同的組,也就是該組內的對象擁有相同的屬性名和屬性值的狀況,將這些屬性名和對應的偏移位置保存在一個隱藏類中,組內的全部對象共享該信息。同時,也能夠識別屬性不一樣的對象。