V8引擎簡介

上一篇(JS引擎、運行時與調用棧概述)主要講了JS引擎、運行時與調用棧的概述。本篇文章將會深刻到谷歌V8 JavaScript引擎的內核部分。咱們也會提供一些怎樣寫出更好的JavaScript代碼的建議。javascript

概覽

一個JavaScript引擎是執行JavaScript代碼的一個程序或者說一個解釋器。一個JavaScript引擎能夠實現成一個標準的解釋器,或者以某種形式將JavaScript編譯成字節碼的即時編譯器。html

如下是一個比較流行的實現JavaScript引擎的項目列表:java

  • V8--開源,由谷歌開發,用C++編寫
  • Rhino--由Mozilla Foundation管理,開源,徹底用Java開發
  • SpiderMonkey--第一個JavaScript引擎,之前用於Netscape瀏覽器,如今用於FireFox瀏覽器
  • JavaScriptCore--開源,做爲Nitro銷售,由Apple爲Safari開發
  • KJS--KDE的引擎,最初由Harri Porten爲KDE項目的Konqueror web瀏覽器開發
  • Chakra(JScript9)--Internet Explorer
  • Chakra(JavaScript)--Microsoft Edge
  • Nashorn--做爲OpenJDK的一部分開源,由Oracle Java Languages and Tool Group 編寫
  • JerryScript--一個物聯網輕量引擎

爲何要建立V8引擎?

由谷歌構建的V8引擎是用C++編寫的開源項目,用於谷歌Chrome內部。然而不像其餘引擎,V8也被用於流行的Node.js運行時。node

V8最開始是爲了提升運行在瀏覽器內部的JavaScript運行性能而設計的。爲了提升速度,V8將JavaScript代碼轉換成更有效率的機器碼,而不是使用一個解釋器。就像其餘一些JavaScript引擎好比SpiderMonkey或Rhino (Mozilla)所作的同樣,V8實現了一個即時(JIT)編譯器在代碼執行時將JavaScript代碼編譯成機器碼。這裏最主要的區別是V8不生成字節碼或其餘中間代碼。git

V8之前有兩種編譯器

在V8 5.9版本出來以前,引擎使用了兩種編譯器:github

  • full-codegen--一個生成簡單和相對較慢機器碼的簡單,速度很快的編譯器
  • Crankshaft--一個生成高度優化代碼的更復雜的(JIT)優化編譯器

V8引擎內部也使用了幾個線程:web

  • 主線程所作的事情就是你所期待的那樣:獲取你的代碼,編譯它而後執行它
  • 也存在另外一個線程用來編譯,那樣當前面正在優化代碼的時候,主線程能夠繼續執行
  • 性能分析線程告訴運行時哪些方法消耗了不少時間,那樣Crankshaft就能去優化它們了
  • 一些處理垃圾回收的線程

最初執行JavaScript代碼的時候,V8使用full-codegen直接將JavaScript轉換成機器碼而沒作任何轉化,這讓引擎能很快開始執行代碼。值得注意的是V8不使用中間字節碼,那就不須要一個解釋器了。編程

當你的代碼運行一段時間後,性能分析線程收集到了足夠的數據來告知哪些方法須要優化。數組

接下來,Crankshaft開始在另外一個線程優化了。它將JavaScript抽象語法樹轉換成一個叫作Hydrogen的高階靜態單賦值形式並試圖優化這個Hydrogen圖,大多數優化都是在這個階段進行的。瀏覽器

內聯

第一步優化是提早內聯儘量多的代碼啊。內聯是將調用地址(函數被調用的代碼行)替換成被調用的函數體。這一簡單的步驟是接下來的優化更有意義。

隱藏的類(Hidden class)

JavaScript是一門基於原型的語言:沒有類,對象是使用克隆來構造的。JavaScript是一門動態編程語言,意味着屬性能夠在對象初始化完成後很容易的被添加或已移除。

大多數JavaScript解釋器使用類字典結構(基於哈希函數)在內存中存儲對象屬性值的位置。這個結構使得在JavaScript中檢索一個屬性的值比在其餘非靜態語言如Java或C#須要更多的計算。在Java中,全部的對象屬性在編譯前就已經被一個固定的對象決定了,並且不能在運行時被動態添加或刪除(好吧,C#有一個動態類型,而這是另一個話題了)。結果是,屬性值(或者指向那些屬性的指針)能夠存儲在內存中一個連續的緩衝區中,彼此之間偏移量是固定的。這個偏移的長度根據屬性的類型能夠很容易的肯定,然而在屬性類型在運行時能夠改變的JavaScript中是不可能的。

由於使用字典在內存中查找對象屬性的位置是很低效的,V8使用一個不一樣的方法來代替:隱藏類(hidden classes)。隱藏類很像在Java中使用的固定對象層(類),除了它們是在運行時建立的。如今讓咱們來看一下它們是什麼樣的:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
複製代碼

只要「new Point(1,2)」被執行,V8就會建立一個隱藏類叫作「C0」。

Point中尚未屬性,因此「C0」是空的。

只要第一個表達式「this.x = x」被執行了(在「Point」函數內部),V8就會建立第二個叫作「C1」的基於「C0」的隱藏類。「C1」描述屬性x在內存中的位置(關聯對象指針)。在這裏,「x」存儲在偏移量爲0的位置上,這意味着當把point對象在內存中看做連續的緩衝區,第一個偏移量指向的是屬性「x」。V8也會用「類轉換」來更新「C0」,代表若是屬性「x」被加入到point對象中,隱藏類就會從 「C0」切換成「C1」。下圖中point對象的隱藏類如今已是「C1」了。

每次有新的屬性被加入到一個對象中,舊的隱藏類就會更新轉換路徑到新的隱藏類。隱藏類轉換是很重要的,由於它們可讓隱藏類在以相同方式建立的對象中共享。若是兩個對象共享一個隱藏類,並且相同的屬性被加入到它們中,轉換就能保證兩個對象得到相同的新隱藏類和隨之攜帶的全部優化過的代碼。

當表達式「this.y = y」被執行的時候(也在Point函數中,「this.x = x」表達式以後),會重複上述過程。

一個叫作「C2」的隱藏類被建立了,一個類型轉換被加入到「C1」中,代表若是有屬性y加入到Point對象中(已經包含屬性「x」),那麼隱藏類將會變爲「C2」,point對象的隱藏類更新爲「C2」。

隱藏類轉換依賴於屬性加入到對象中的順訊,看一下以下代碼片斷:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
複製代碼

如今你可能會假定p1和p2會使用相同的隱藏類和轉換。然而並非如此。對「p1」來講,屬性「a」先會被加入,而後是屬性「b」。而「p2」,「b」先被賦值,而後是「a」。這樣,「p1」和「p2」結果會使用不一樣的隱藏類和不一樣的轉換路徑。所以,最好是使用相同的順序來初始化動態屬性,那樣隱藏類能夠被重複使用。

內聯緩存

V8利用另外一項叫作內聯緩存的技術來優化動態類型語言。內聯緩存依賴於發生在相同對象類型上的相同方法的重複調用。一個內聯緩存的深度解釋能夠在這裏找到。

咱們將會說一下內聯緩存的通用概念(以防你沒有時間去看上面的深度解釋)。

內聯緩存是如何工做的呢?V8維護一個在最近的屢次方法調用中傳遞的參數的對象類型的緩存,用這個信息來假設將來傳遞的參數的對象類型。若是V8能夠很好的假設傳遞給方法的對象類型,它就能夠繞過推測怎樣訪問對象屬性的過程,取而代之,使用前面保存的信息來查找到對象的隱藏類。

那麼隱藏類和內聯緩存是怎樣關聯起來的呢?當調用一個特定對象的方法時,V8引擎必須查找到那個對象的隱藏類,來肯定訪問特定屬性的偏移量。當兩次成功調用相同緩存類的相同方法以後,V8將會略過隱藏類的查找,只是將屬性的偏移量加入到對象指針中。在之後那個方法的全部調用中,V8引擎都會假定隱藏類沒有改變,而使用前面查找中存儲的偏移量直接跳到那個特定屬性的內存地址上。這大幅度的提升了執行速度。

內聯緩存也是相同類型的對象共享內聯緩存如此重要的緣由。若是你建立兩個類型相同,隱藏類不一樣的對象(就像以前的例子那樣),V8將不會使用內聯緩存,由於即便兩個對象類型相同,但它們對應的隱藏類給它們的屬性分配了不一樣的偏移量。

兩個對象基本相同,但「a」和「b」屬性是以不一樣的順序建立的。

編譯成機器碼

只要Hydrogen圖被優化過了,Crankshaft就會將它降成叫作Lithium的低階形式。大多數Lithium實現是特定結構的。註冊內存分配發生在這個階段。

最後Lithium被編譯成機器碼。而後叫OSR:棧上替換(on-stack replacement)的事情發生了。在咱們編譯和優化一個明顯運行很長時間的方法以前,咱們可能先要運行它。V8不會忘記剛剛運行緩慢的方法,會使用優化後的版原本運行它。取而代之,它將會轉換咱們擁有的全部上下文(棧,註冊器),那樣咱們就能夠在執行的中間替換成優化後的版本。這是一個很是複雜的任務,記住在其餘優化中,V8在最開始就已經內聯了代碼。V8不是惟一一個這樣作的引擎。

有個叫去優化的保護措施,用相反的轉化將代碼恢復成未優化的代碼,以防引擎的假設再也不正確。

垃圾回收

對於垃圾回收,V8使用了傳統的標記-清除分代方法來清除老生代。標記階段會中止JavaScript的執行。爲了控制GC消耗,使執行更加穩定,V8使用遞增標記:不遍歷整個堆,而是試圖標記可能的對象,它只遍歷堆的一部分,而後恢復正常的執行。下一次GC將會從上次堆遍歷中止的地方開始。這隻會形成在正常執行中的很短暫的暫停。就像前面提到的,清除階段將會由另外的線程處理。

Ignition and TurboFan

在2017年發佈的V8 5.9版本,引進了一個新的執行管道。這個新的管道在真實的JavaScript項目中取得了更加大的性能改善和顯著的內存節省。

這個新的管道構建在Ignition, V8的解釋器和TurboFan,V8最新的優化編譯器的頂上。

你能夠在這裏看到V8團隊關於這個主題的博客。

自從V8的5.9版本發佈依賴,full-codegen和Crankshaft(自從2010年就開始服務於V8的技術)就再也不被V8做爲JavaScript的執行所使用,由於V8團隊須要盡力跟上新的JavaScript語言特性和知足這些特性的優化須要。

這意味着整體上V8在將來將會擁有簡單的多和可維護度更高的體系結構。

在Web和node.js基準線上的改善

這些改善只是開始。新的Ignition和TurboFan管道爲未來的優化鋪好了道路,這樣會大幅度提升JavaScript的性能,使V8在接下來的許多年在Chrome和Node.js上踩下更堅實的足跡。

最後,這裏有一些怎樣寫出良好優化的更好的JavaScript的一些建議和技巧。你能夠很容易根據上面的內容得出這些結論,這裏只是爲了你的方便,總結一下:

怎樣寫出性能優化的代碼

  1. 對象屬性的順序:老是以相同的順序實例化對象屬性,那樣隱藏類和接下來的優化代碼可以被共享。
  2. 動態屬性:在實例化後添加屬性到一個對象中將會強制改變隱藏類,拖慢以前爲隱藏類優化過的任何方法。取而代之,在構造函數中爲對象全部的屬性賦值。
  3. 方法:重複運行相同方法的代碼會比每次運行不一樣的方法快一些(由於內聯緩存)
  4. 數組:避免使用key值不是遞增數字的稀疏數組。不是每一個元素都在內部的稀疏數組是一個哈希表。這種數組的元素須要消耗更多資源才能訪問到。也要避免提早分配大數組,最好是用到才分配。最後,不要刪除數組的元素,這樣會讓key變得稀疏。
  5. 標籤值:V8用32位來表示對象和數字。它用一位來表示是一個對象(flag = 1)或一個數字(flag = 0),被稱做是SMI(SMall Integer),由於它的31位。而後,若是一個數字值比31爲要大,V8將會對number進行裝箱,將它轉換成一個double類型,建立一個新對象來把它放進去。在任什麼時候候試着使用31位有符號數字來避免很昂貴的進入一個JS對象的裝箱操做。

本文翻譯自:blog.sessionstack.com/how-javascr…

相關文章
相關標籤/搜索