譯—JavaScript是如何工做的(2):V8引擎內部+優化代碼的5個技巧

幾周以前,咱們開始了一系列旨在深刻挖掘JavaScript及其實際工做原理的文章:咱們認爲經過了解JavaScript的構建塊以及它們如何共同發揮做用,你將可以編寫更好的代碼和應用程​​序。                                                                                                                                      javascript

第一篇文章集中於提供引擎,運行時和調用堆棧的概述。第二篇文章將深刻探討谷歌V8 JavaScript引擎的內部。咱們還將提供一些關於如何編寫更好的JavaScript代碼的小建議—最佳實踐就是咱們的團隊構建的SessionStack,在開發時咱們就遵循了這些tips。html

概述

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

這是一個實現JavaScript引擎的熱門項目列表:
git

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

V8引擎是如何產生的?

由谷歌構建的V8引擎是開源的,用C ++編寫。此引擎在Google Chrome中使用。然而,與其餘引擎不一樣,V8也用於流行的Node.js運行時。
github


V8最初是被設計來提升JavaScript在web瀏覽器中執行的性能的。爲了提升速度,V8將JavaScript代碼轉換爲更高效的機器代碼,而不是使用解釋器。它經過實現JIT(Just-in-Time) 編譯器在執行時將JavaScript代碼編譯成機器代碼來實現的,就像大多數現代JavaScript引擎所作的那樣,好比SpiderMonkey or Rhino (Mozilla)。主要區別是V8不產生字節碼或任何中間代碼。web

V8曾經有兩個編譯器

  • full-codegen— 一個簡單而快速的編譯器,能夠生成簡單且相對較慢的機器代碼。
  • Crankshaft - 一種更復雜的(即時)優化編譯器,可生成高度優化的代碼。
V8引擎內部還使用了幾個線程:
  • 主線程完成你的指望:獲取代碼,編譯代碼而後執行它
  • 還有一個單獨的線程用於編譯,以保證主線程能夠在前者(應該指的是Crankshaft )優化代碼的同時可以繼續執行。
  • 一個Profiler線程,它將告訴運行時哪個方法花費了大量時間,以便Crankshaft能夠優化它們
  • 一些線程來處理垃圾收集器的掃描。
當第一次執行JavaScript代碼的時候,V8利用全代碼生成器,直接將解析後的JavaScript翻譯爲機器代碼而不作其餘任何的轉換。這使它能夠很是快速地開始執行機器代碼。請注意,V8不使用中間字節碼的這種方式使其不須要解釋器這種東西。


當代碼運行一段時間後,探查線程(profiler thread)已經收集了足夠的數據來告訴(Crankshaft )應該優化哪一個方法。
編程

接下來, Crankshaft 優化 開啓了另外一個線程。它將JavaScript抽象語法樹轉換爲名爲Hydrogen的高級靜態單指派(SSA)表現(a high-level static single-assignment (SSA) representation),並嘗試優化氫圖(Hydrogen graph)。大多數優化都是在這個級別完成的。數組

內聯(Inlining)

第一次優化是儘量提早內聯更多的代碼。內聯就是一個用函數體替換函數調用點(函數被調用的代碼行)的過程。這個簡單的步驟使接下來的優化更有意義。瀏覽器

             

Hidden class(隱藏類)

JavaScript是一門基於原型的語言:沒有類和對象是使用克隆過程建立的。JavaScript也是一種動態編程語言,這意味着在實例化後能夠輕鬆地在對象中添加或刪除屬性。緩存

大多數JavaScript解釋器使用相似字典的結構(基於hash function)來存儲對象屬性值在內存中的位置。這種結構使得在JavaScript中檢索屬性的值比在Java或C#等非動態編程語言中的計算成本更高。在Java裏,全部對象屬性都是在編譯以前由固定對象佈局肯定的,而且沒法在運行時動態添加或刪除(固然,C#也有動態類型,這又是另外一個話題了。)結果是,屬性值(或指向這些屬性的指針)能夠以兩兩之間有一個固定的偏移量(fixed-offset )做爲連續緩衝區存儲在內存中。這些偏移(offset )的長度能夠根據屬性類型輕鬆肯定,而這在運行時能夠更改屬性類型的JavaScript中是不可能的。

因爲使用字典來查找內存中對象屬性的位置是很是低效的,V8使用了一個不一樣的方法來代替:hidden classes(隱藏類)。隱藏類的工做方式相似於Java等語言中使用的固定對象佈局(類),除非它們是在運行時建立的。如今,讓咱們看看它們其實是什麼樣的:

function(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」。(這句我以爲翻譯可能有不許的地方,原句是:In this case, 「x」 is stored at offset 0, which means that when viewing a point object in the memory as a continuous buffer, the first offset will correspond to property 「x」.)

V8也會用「類轉換」(「class transition」 )更新「C0」,該類轉換指出若是將屬性「x」添加到Point 對象,則隱藏類應該從「C0」切換到「C1」。下面的Point對象的隱藏類如今是「C1」。


每次將新屬性添加到對象時,舊的隱藏類都會更新爲新隱藏類的轉換路徑。隱藏類轉換很重要,由於它們容許在以相同方式建立的對象之間共享隱藏類。若是兩個對象共享一個隱藏類而且同一屬性被添加到它們中,則轉換要確保兩個對象都接收相同的新隱藏類以及隨其附帶的全部優化代碼。

執行語句「this.y = y」(在Point函數內,在「this.x = x」語句以後)時重複此過程。

一個新的隱藏類「C2」被建立了,一個新的聲明瞭若是將屬性「y」添加到Point對象(已包含屬性「x」),則隱藏的類應更改成「C2」的類轉換被添加給C1,而且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」以不一樣的隱藏類結束。在這些狀況下,以相同的順序初始化動態屬性要好得多,以即可以重用隱藏的類。

內聯緩存(Inline caching)

V8利用另外一種技術優化動態類型語言,稱爲內聯緩存。內聯緩存依賴於觀察到對相同類型的對象的重複調用傾向於發生在相同類型的對象上。

能夠在這裏找到有關內聯緩存的深刻說明。

咱們將討論內聯緩存的一些基礎概念(若是你沒有時間瀏覽上面的深刻解釋的文章)。

那麼它是怎麼工做的呢?V8維持一個在最近的方法調用中做爲參數傳遞的對象類型的緩存,並使用此信息來假設未來做爲參數傳遞的對象類型。若是V8可以對將傳遞給方法的對象類型作出很好的假設,它能夠繞過弄清楚如何訪問對象屬性的過程,而是使用先前查找中存儲的信息到對象的隱藏類。

那麼隱藏類和內聯緩存的概念之間有什麼關係呢?每當在特定對象上調用方法時,V8引擎必須執行對該對象的隱藏類的查找,以肯定訪問特定屬性的偏移量。在將同一方法兩次成功調用到同一個隱藏類以後,V8省略了隱藏類查找,只是簡單地將屬性的偏移量添加到對象指針自己。對於該方法接下來的調用,V8引擎假定隱藏類沒有改變,而後使用先前查找中存儲的偏移量直接跳轉到特定屬性的內存地址。這大大提升了執行速度。

內聯緩存也是爲何相同類型的對象共享隱藏類很是重要的緣由。若是你建立兩個相同類型且具備不一樣隱藏類的對象(如前面示例中所作的那樣),V8沒法使用內聯緩存,由於即便兩個對象屬於同一類型,其對應的隱藏類也會爲其屬性分配不一樣的偏移量。


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

編譯到機器代碼

氫圖(Hydrogen graph)優化後,Crankshaft將其下降到稱爲鋰(Lithium)的低級別表示。大多數Lithium實現都是特定於某種結構的。寄存器的分配(Register allocation)就發生在此級別。

在最後,Lithium被編譯成機器代碼。而後發生了一些其餘的叫作OSR的事情:堆棧替換。在咱們開始編譯和優化一個明顯長期運行的方法以前,咱們可能正在運行它。V8不會忘記它只是慢慢執行以便再次使用優化版本。相反,它將轉換咱們擁有的全部上下文(堆棧,寄存器),以便咱們能夠在執行過程當中切換到優化版本。這是一項很是複雜的任務,請記住,除了其餘優化以外,V8最初還內聯了代碼。 V8並非惟一可以作到這一點的引擎。

有一種稱爲去優化的保護措施能夠進行相反的轉換,並在引擎的假設再也不適用的狀況下恢復到非優化代碼。

垃圾收集(Garbage collection)

在垃圾處理這一塊,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的提示和技巧。你固然能夠從上面的內容輕鬆地推導出這些內容,可是,這裏有一個方便的總結:

How to write optimized JavaScript

  1. 對象屬性的順序:始終以相同的順序實例化您的對象屬性,以即可以共享隱藏的類和隨後優化的代碼。
  2. 動態屬性:在實例化以後向對象添加屬性會強制隱藏類改變並減緩爲先前隱藏類優化的任何方法。所以,在其構造函數中分配全部對象的屬性。
  3. 方法:重複執行相同方法的代碼將比僅執行一次不一樣方法的代碼運行得更快(因爲內聯緩存)。
  4. 數組:避免鍵不是增量數的稀疏數組。其中沒有每一個元素的稀疏數組是一個哈希表(hash table)。這種數組中的元素訪問起來更加昂貴(費時、麻煩)。另外,儘可能避免預先分配的大數組。隨着須要增加其長度更好。最後,不要刪除數組中的元素。這會使鍵變得稀疏。
  5. 標記值Tagged values):V8的對象和數字都用32位來表示。它使用一個位來知道它是一個對象(flag = 1)仍是一個稱爲SMI(SMall Integer)的31位的整數(flag = 0)。而後,若是數值大於31位,V8會將數字打包,將其變爲雙精度並建立一個新對象以將數字放入其中。嘗試儘量使用31位帶符號的數字,以免對JS對象進行昂貴的裝箱操做。
咱們在SessionStack中嘗試遵循這些作法以寫出高度優化的JavaScript代碼。緣由是,一旦你將SessionStack集成到你的Web應用程序中,它將開始記錄一切:全部DOM更改,用戶交互,JavaScript異常,堆棧跟蹤,失敗的網絡請求和調試消息。

使用SessionStack,你能夠將網絡應用中的問題做爲視頻重播,並查看用戶發生的全部事情。而且全部的這一切都不會影響你的web應用性能。

這裏能夠免費試用。get started for free.


Resources

  • https://docs.google.com/document/u/1/d/1hOaE7vbwdLLXWj3C8hTnnkpE0qSa2P--dtDvwXXEeD0/pub
  • https://github.com/thlorenz/v8-perf
  • http://code.google.com/p/v8/wiki/UsingGit
  • http://mrale.ph/v8/resources.html
  • https://www.youtube.com/watch?v=UJPdhx5zTaw
  • https://www.youtube.com/watch?v=hWhMKalEicY
相關文章
相關標籤/搜索