摘要:本文將會深入分析的引擎的內部實現。該引擎使用在谷歌瀏覽器內部。同其他現代引擎如或所做的一樣,通過實現即時編譯器在執行時將代碼編譯成機器代碼。這可使正常執行期間只發生相當短的暫停。
原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
幾周前我們開始了一個系列博文旨在深入挖掘 JavaScript 并弄清楚它的工作原理:我們認為通過了解 JavaScript 的構建單元并熟悉它們是怎樣結合起來的,有助于寫出更好的代碼和應用。
這個系列的第一篇文章聚焦于提供一個關于引擎、運行時和調用棧的概述。本文將會深入分析 Google 的 V8 引擎的內部實現。我們也會提供一些編寫更優質 JavaScript 代碼的小技巧——我們的團隊在構建 SessionStack 應用時遵循的最佳實踐。
概述JavaScript 引擎是執行 JavaScript 代碼的程序或解釋器。 JavaScript 引擎可以實現為標準的解釋器,或即時編譯器,以某種形式將 JavaScript 編譯成字節碼。
以下是一些流行的 JavaScript 引擎項目:
V8 —— 開源,Google 開發,C++ 編寫
Rhino ?—— Mozilla 基金會管理,開源,完全使用 Java 開發
SpiderMonkey —— 第一個 JavaScript 引擎,以前由 Netscape Navigator 維護,現在由 Firefox 維護
JavaScriptCore —— 開源,以 Nitro 的名義銷售,由 Apple 公司為 Safari 瀏覽器開發
KJS? —— ?KDE 的引擎,最初由 Harri Porten 為 ?KDE 項目的 Konqueror 瀏覽器開發
Chakra (JScript9)? —— IE 瀏覽器
Chakra (JavaScript)? —— Edge 瀏覽器
Nashorn —— OpenJDK 開源項目的一部分,由 Oracle Java 和其工具集開發
JerryScript? —— 一個輕量級的物聯網引擎
為什么要創建V8引擎?谷歌公司研發的 V8 引擎是由 C++ 編寫的開源引擎。該引擎使用在谷歌瀏覽器內部。但與其他引擎不同的是,V8 也應用于 Node.js 這一流行的運行時當中。
V8 最初是為了提高瀏覽器中 JavaScript 執行的性能而設計的。為了獲得速度,V8 將 JavaScript 代碼轉換成更高效的機器編碼而不是使用解釋器。同其他現代 JavaScript 引擎如 SpiderMonkey 或 Rhino (Mozilla)所做的一樣,V8 通過實現即時編譯器在執行時將 JavaScript 代碼編譯成機器代碼。其中最主要的區別是 V8 不生成字節碼或任何中間代碼。
V8曾有兩個編譯器在 V8 5.9版本發布之前(2017年初發布),該引擎使用兩個編譯器:
full-codegen —— 簡單、非常快的編譯器,生成簡單和相對較慢的機器代碼
Crankshaft ?—— 更加復雜的(即時)優化編譯器,生成高度優化的代碼
同時 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 采用增量標記:它不遍歷全部棧堆,而是嘗試標記每一個可能的對象,它只遍歷棧堆的一部分,然后恢復正常執行。下一次垃圾回收暫停會在之前棧堆的停止位置繼續。這可使正常執行期間只發生相當短的暫停。正如之前提到的,清理階段由多帶帶的線程處理。
Ignition 和 TurboFan隨著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 代碼的建議和技巧。雖然你可以很容易地從上述的內容中得到這些,為了方便還是把它們做了以下的總結:
怎么編寫優化的JavaScript對象屬性的順序:始終使用相同的順序初始化對象屬性,以便共享隱藏類和隨后的優化代碼。
動態屬性:在初始化完成之后添加對象動態屬性會強制改變隱藏類并使之前的隱藏類已優化的方法變慢。相反,在對象的構造器里指定所有的屬性。
方法:重復執行相同方法的代碼會比僅執行一次許多不同的方法運行的更快(由于內聯緩存)。
數組:避免使用鍵值不遞增的稀疏數組。并非每個元素都存在的稀疏數組是一個哈希表。訪問稀疏數組的元素將會花費更昂貴的開銷。此外,避免預先分配大數組。最好是按需要增加長度。最后,不要刪除數組中的元素。這會使數組變得稀疏。
帶標記的值:V8 用32位字節表示對象和數字。其中使用了一個位來標識是對象(標識為1)或是整數(標識為0),由于它們是31位的而被稱為 SMI(SMall Integer)。如果一個數值大小超過了31位可以表示的數字,V8 將會包裝它,將其轉換為一個雙字節類型值并創建一個新的對象存入其中。盡量使用31帶符號的數值避免 JS 對象的昂貴包裝操作。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105009.html
摘要:引擎可以用標準解釋器或即時編譯器來實現,即時編譯器以某種形式將代碼編譯為字節碼。這里的主要區別在于不生成字節碼或任何中間代碼。請注意,不使用中間字節碼表示法,不需要解釋器。這允許在正常執行期間非常短的暫停。 本系列的第一篇文章重點介紹了引擎,運行時和調用棧的概述。第二篇文章將深入V8的JavaScript引擎的內部。我們還會提供一些關于如何編寫更好的JavaScript代碼的技巧。 概...
摘要:引擎可以是一個標準的解釋器,也可以是一個將編譯成某種形式的字節碼的即時編譯器。和其他引擎最主要的差別在于,不會生成任何字節碼或是中間代碼。不使用中間字節碼的表示方式,就沒有必要用解釋器了。 原文地址:https://blog.sessionstack.com... showImg(https://segmentfault.com/img/bVVwZ8?w=395&h=395); 數周之...
摘要:本章將會深入谷歌引擎的內部結構。一個引擎可以用標準解釋程序或者即時編譯器來實現,即時編譯器即以某種形式把解釋為字節碼。引擎的由來引擎是由谷歌開源并以語言編寫。注意到沒有使用中間字節碼來表示,這樣就不需要解釋器了。 原文請查閱這里,略有刪減。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第二章。 本章將會深入谷歌 V8 引擎的內部結構。我們也會...
摘要:調用棧是單線程編程語言,意味著它只有單一的調用棧。調用棧是一種數據結構,基本記錄了程序運行的位置。舉個例子,先來看如下所示的代碼當引擎開始執行這段代碼時,調用棧將是空的。這正是拋出異常時棧追蹤的構造過程這基本上就是異常拋出時調用棧的狀態。 原文 How JavaScript works: an overview of the engine, the runtime, and the c...
摘要:這些是中可用的最快屬性。通常來說我們將線性屬性存儲中存儲的屬性稱為。因此也支持所謂的屬性。整數索引屬性的處理和命名屬性的復雜性相同。 本文為譯文,原文地址:http://v8project.blogspot.com...,作者,@Camillo Bruni ,V8 JavaScript Engine Team Blog 在這篇博客中,我們想解釋 V8 如何在內部處理 JavaScrip...
閱讀 964·2023-04-26 02:56
閱讀 9437·2021-11-23 09:51
閱讀 1849·2021-09-26 10:14
閱讀 2979·2019-08-29 13:09
閱讀 2153·2019-08-26 13:29
閱讀 571·2019-08-26 12:02
閱讀 3561·2019-08-26 10:42
閱讀 3000·2019-08-23 18:18