原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized codejavascript
幾周前咱們開始了一個系列博文旨在深刻挖掘 JavaScript
並弄清楚它的工做原理:咱們認爲經過了解 JavaScript
的構建單元並熟悉它們是怎樣結合起來的,有助於寫出更好的代碼和應用。html
這個系列的第一篇文章聚焦於提供一個關於引擎、運行時和調用棧的概述。本文將會深刻分析 Google
的 V8
引擎的內部實現。咱們也會提供一些編寫更優質 JavaScript
代碼的小技巧——咱們的團隊在構建 SessionStack
應用時遵循的最佳實踐。java
JavaScript
引擎是執行 JavaScript
代碼的程序或解釋器。 JavaScript
引擎能夠實現爲標準的解釋器,或即時編譯器,以某種形式將 JavaScript
編譯成字節碼。git
如下是一些流行的 JavaScript
引擎項目:github
Google
開發,C++
編寫Mozilla
基金會管理,開源,徹底使用 Java
開發JavaScript
引擎,之前由 Netscape Navigator
維護,如今由 Firefox
維護Nitro
的名義銷售,由 Apple
公司爲 Safari
瀏覽器開發KDE
的引擎,最初由 Harri Porten
爲 KDE
項目的 Konqueror
瀏覽器開發IE
瀏覽器Edge
瀏覽器OpenJDK
開源項目的一部分,由 Oracle Java
和其工具集開發谷歌公司研發的 V8
引擎是由 C++
編寫的開源引擎。該引擎使用在谷歌瀏覽器內部。但與其餘引擎不一樣的是,V8
也應用於 Node.js
這一流行的運行時當中。編程
V8
最初是爲了提升瀏覽器中 JavaScript
執行的性能而設計的。爲了得到速度,V8
將 JavaScript
代碼轉換成更高效的機器編碼而不是使用解釋器。同其餘現代 JavaScript
引擎如 SpiderMonkey
或 Rhino
(Mozilla
)所作的同樣,V8
經過實現即時編譯器在執行時將 JavaScript
代碼編譯成機器代碼。其中最主要的區別是 V8
不生成字節碼或任何中間代碼。數組
在 V8
5.9版本發佈以前(2017年初發布),該引擎使用兩個編譯器:瀏覽器
同時 V8
內部使用了多條線程:緩存
Crankshaft
編譯器優化代碼首次執行 JavaScript
代碼時,V8
利用 full-codegen
無過渡地直接將解析後的 JavaScript
轉換成機器代碼。這使得它能夠很是快速地開始執行機器代碼。注意 V8
不使用中間代碼表示,所以擺脫了對解釋器的須要。安全
在你的代碼運行了必定時間後,分析線程就能收集到足夠的數據判斷哪些方法須要優化。
接着,Crankshaft
優化在另外一線程開始。它將 JavaScript
抽象語法樹轉換成高級靜態單賦值(SSA
)表示,稱爲 Hydrogen
(注:氮),並嘗試優化氮圖。大多數優化都在這個級別完成。
優化的第一步是先內聯儘量多的代碼。內聯是一個將調用引用(函數調用的那行代碼)替換成所調用的函數體的過程。這個簡單的步驟使接下來的優化過程更有意義:
JavaScript
是基於原型的語言:沒有類,使用克隆的方式建立對象。JavaScript
仍是一個動態編程語言,這意味着當對象被初始化以後還能夠輕易地增刪其屬性。
大多數 JavaScript
解釋器採用類字典數據結構(基於哈希函數)來存儲對象屬性值在內存中的位置。這種結構使得在 JavaScript
中取回屬性值的計算開銷比非動態語言如 Java
或 C#
更昂貴。在 Java
中,全部的對象屬性在編譯前就由固定對象佈局決定了,不容許在運行時動態增長或刪除(C#
有動態類型,但那是另外一個話題)。所以,屬性值(或指向屬性的指針)就能夠以連續緩衝區存儲在內存中,之間用固定的偏移量隔開。偏移量的長度簡單地根據屬性的類型肯定,然而這在 JavaScript
中是不可能的,由於屬性類型能夠在運行時更改。
因爲經過字典查找對象屬性在內存中的位置很是低效,V8
採用了另外一方法做爲替代:隱藏類。隱藏類的原理相似於 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
將建立第二個基於 C0
的隱藏類 C1
。C1
描述了在內存中(相對於 point
對象)能找到屬性 x
的位置。在這個例子中,x
保存在偏移量爲 0
的位置,這意味着在將內存中的對象視做一個連續緩衝區時,第一個偏移量對應着 x
。V8
還會經過一個「類轉換」更新 C0
,以代表若是一個屬性 x
被添加到 point
對象中,隱藏類 C0
就會轉換成 C1
。下面 point
對象的隱藏類如今變成了 C1
。
每次添加一個新屬性到對象,舊隱藏類都會經過一個轉換路徑更新成一個新隱藏類。隱藏類轉換之因此如此重要是由於它能使隱藏類在以一樣方式建立的對象間共享。若是兩個對象共享同一個隱藏類並向它們添加相同的屬性,轉換能夠確保它們得到相同的隱藏類和全部與其相關的優化代碼。
當 this.y = y
語句執行時將會重複一樣的過程(一樣在 Point
函數內,this.x = x
以後)。
新的隱藏類 C2
將被建立,C1
發生類轉換表示若是向一個 Point
對象添加屬性 y
(已經包含一個屬性 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
屬性的建立順序不一樣。
一旦氮圖優化好後,Crankshaft
會將它降爲更低水平的表示,稱爲 Lithium
(注:鋰)。大多數 Lithium
的實現依賴於特定架構。寄存器分配發生在這個級別。
最終,Lithium
被編譯成機器代碼。隨後發生 OSR
:堆棧上替換。在開始編譯和優化明顯長時間運行的方法前,咱們可能會運行它。V8
不會在再次開始執行優化版本時忘記那些緩慢的執行。而是轉換咱們全部的上下文(棧,寄存器)以便能在執行中切換到優化版本。這是個很是複雜的任務,記住在其餘的優化中,V8
最早作了代碼內聯。V8
不是惟一有這種能力的引擎。
還有種被稱爲反優化的安全措施能作反向轉換,回退到未優化代碼,以防引擎作出的假設再也不成立。
在垃圾回收方面,V8
採用傳統分代方法標記和清掃來清理老的代。標記階段會暫停 JavaScript
的執行。爲了控制垃圾回收的開銷並使執行更加穩定,V8
採用增量標記:它不遍歷所有棧堆,而是嘗試標記每個可能的對象,它只遍歷棧堆的一部分,而後恢復正常執行。下一次垃圾回收暫停會在以前棧堆的中止位置繼續。這可以使正常執行期間只發生至關短的暫停。正如以前提到的,清理階段由單獨的線程處理。
隨着2017年初 V8
5.9版本的發佈,一個新的執行管道被引入。新的管道在實際的JavaScript
應用中實現了更大的性能提高和的顯著的內存節省。
新的執行管道構建在 V8
的解釋器 Ignition
和 V8
最新的優化編譯器 TurboFan
之上。
你能夠在這裏查閱 V8
團隊關於這個主題的博文。
自從 V8
5.9版本發佈以來, V8
就再也不在 JavaScript
執行裏使用 full-codegen
和 Crankshaft
(自2010年來一直支撐着 V8
的技術),這是因爲 V8
團隊也在努力地跟上新的 JavaScript
語言特性的腳步和這些特性所需的優化。
這意味着未來在總體上 V8
將擁有更加簡單和更易於維護的架構。
這些提高僅僅是個開始。新的 Ignition
和 TurboFan
管道鋪墊了更遠的優化之路,將會推動 JavaScript
的性能並在接下來的幾年裏縮小 V8
在 Chrome
和 Node.js
中的足跡。
最後,這裏有幾條關於如何編寫更優化的、更好的 JavaScript
代碼的建議和技巧。雖然你能夠很容易地從上述的內容中獲得這些,爲了方便仍是把它們作了如下的總結:
V8
用32位字節表示對象和數字。其中使用了一個位來標識是對象(標識爲1)或是整數(標識爲0),因爲它們是31位的而被稱爲 SMI
(SMall Integer
)。若是一個數值大小超過了31位能夠表示的數字,V8
將會包裝它,將其轉換爲一個雙字節類型值並建立一個新的對象存入其中。儘可能使用31帶符號的數值避免 JS
對象的昂貴包裝操做。