摘要:本文將會(huì)深入分析的引擎的內(nèi)部實(shí)現(xiàn)。該引擎使用在谷歌瀏覽器內(nèi)部。同其他現(xiàn)代引擎如或所做的一樣,通過實(shí)現(xiàn)即時(shí)編譯器在執(zhí)行時(shí)將代碼編譯成機(jī)器代碼。這可使正常執(zhí)行期間只發(fā)生相當(dāng)短的暫停。
原文 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
幾周前我們開始了一個(gè)系列博文旨在深入挖掘 JavaScript 并弄清楚它的工作原理:我們認(rèn)為通過了解 JavaScript 的構(gòu)建單元并熟悉它們是怎樣結(jié)合起來的,有助于寫出更好的代碼和應(yīng)用。
這個(gè)系列的第一篇文章聚焦于提供一個(gè)關(guān)于引擎、運(yùn)行時(shí)和調(diào)用棧的概述。本文將會(huì)深入分析 Google 的 V8 引擎的內(nèi)部實(shí)現(xiàn)。我們也會(huì)提供一些編寫更優(yōu)質(zhì) JavaScript 代碼的小技巧——我們的團(tuán)隊(duì)在構(gòu)建 SessionStack 應(yīng)用時(shí)遵循的最佳實(shí)踐。
概述JavaScript 引擎是執(zhí)行 JavaScript 代碼的程序或解釋器。 JavaScript 引擎可以實(shí)現(xiàn)為標(biāo)準(zhǔn)的解釋器,或即時(shí)編譯器,以某種形式將 JavaScript 編譯成字節(jié)碼。
以下是一些流行的 JavaScript 引擎項(xiàng)目:
V8 —— 開源,Google 開發(fā),C++ 編寫
Rhino ?—— Mozilla 基金會(huì)管理,開源,完全使用 Java 開發(fā)
SpiderMonkey —— 第一個(gè) JavaScript 引擎,以前由 Netscape Navigator 維護(hù),現(xiàn)在由 Firefox 維護(hù)
JavaScriptCore —— 開源,以 Nitro 的名義銷售,由 Apple 公司為 Safari 瀏覽器開發(fā)
KJS? —— ?KDE 的引擎,最初由 Harri Porten 為 ?KDE 項(xiàng)目的 Konqueror 瀏覽器開發(fā)
Chakra (JScript9)? —— IE 瀏覽器
Chakra (JavaScript)? —— Edge 瀏覽器
Nashorn —— OpenJDK 開源項(xiàng)目的一部分,由 Oracle Java 和其工具集開發(fā)
JerryScript? —— 一個(gè)輕量級(jí)的物聯(lián)網(wǎng)引擎
為什么要?jiǎng)?chuàng)建V8引擎?谷歌公司研發(fā)的 V8 引擎是由 C++ 編寫的開源引擎。該引擎使用在谷歌瀏覽器內(nèi)部。但與其他引擎不同的是,V8 也應(yīng)用于 Node.js 這一流行的運(yùn)行時(shí)當(dāng)中。
V8 最初是為了提高瀏覽器中 JavaScript 執(zhí)行的性能而設(shè)計(jì)的。為了獲得速度,V8 將 JavaScript 代碼轉(zhuǎn)換成更高效的機(jī)器編碼而不是使用解釋器。同其他現(xiàn)代 JavaScript 引擎如 SpiderMonkey 或 Rhino (Mozilla)所做的一樣,V8 通過實(shí)現(xiàn)即時(shí)編譯器在執(zhí)行時(shí)將 JavaScript 代碼編譯成機(jī)器代碼。其中最主要的區(qū)別是 V8 不生成字節(jié)碼或任何中間代碼。
V8曾有兩個(gè)編譯器在 V8 5.9版本發(fā)布之前(2017年初發(fā)布),該引擎使用兩個(gè)編譯器:
full-codegen —— 簡單、非??斓木幾g器,生成簡單和相對(duì)較慢的機(jī)器代碼
Crankshaft ?—— 更加復(fù)雜的(即時(shí))優(yōu)化編譯器,生成高度優(yōu)化的代碼
同時(shí) V8 內(nèi)部使用了多條線程:
主線程的工作正如你所預(yù)期:獲取代碼、編譯然后執(zhí)行代碼
另有一條獨(dú)立線程負(fù)責(zé)編譯,這樣主線程可以在前者優(yōu)化代碼時(shí)繼續(xù)執(zhí)行
一條分析器線程會(huì)告訴運(yùn)行時(shí),哪些方法會(huì)耗費(fèi)大量時(shí)間以便 Crankshaft 編譯器優(yōu)化代碼
還有幾條線程處理垃圾回收清理
首次執(zhí)行 JavaScript 代碼時(shí),V8 利用 full-codegen 無過渡地直接將解析后的 JavaScript 轉(zhuǎn)換成機(jī)器代碼。這使得它可以非??焖俚亻_始執(zhí)行機(jī)器代碼。注意 V8 不使用中間代碼表示,因此擺脫了對(duì)解釋器的需要。
在你的代碼運(yùn)行了一定時(shí)間后,分析線程就能收集到足夠的數(shù)據(jù)判斷哪些方法需要優(yōu)化。
接著,Crankshaft 優(yōu)化在另一線程開始。它將 JavaScript 抽象語法樹轉(zhuǎn)換成高級(jí)靜態(tài)單賦值(SSA)表示,稱為 Hydrogen(注:氮),并嘗試優(yōu)化氮圖。大多數(shù)優(yōu)化都在這個(gè)級(jí)別完成。
內(nèi)聯(lián)優(yōu)化的第一步是先內(nèi)聯(lián)盡可能多的代碼。內(nèi)聯(lián)是一個(gè)將調(diào)用引用(函數(shù)調(diào)用的那行代碼)替換成所調(diào)用的函數(shù)體的過程。這個(gè)簡單的步驟使接下來的優(yōu)化過程更有意義:
隱藏類JavaScript 是基于原型的語言:沒有類,使用克隆的方式創(chuàng)建對(duì)象。JavaScript 還是一個(gè)動(dòng)態(tài)編程語言,這意味著當(dāng)對(duì)象被初始化之后還可以輕易地增刪其屬性。
大多數(shù) JavaScript 解釋器采用類字典數(shù)據(jù)結(jié)構(gòu)(基于哈希函數(shù))來存儲(chǔ)對(duì)象屬性值在內(nèi)存中的位置。這種結(jié)構(gòu)使得在 JavaScript 中取回屬性值的計(jì)算開銷比非動(dòng)態(tài)語言如 Java 或 C#更昂貴。在 Java 中,所有的對(duì)象屬性在編譯前就由固定對(duì)象布局決定了,不允許在運(yùn)行時(shí)動(dòng)態(tài)增加或刪除(C#有動(dòng)態(tài)類型,但那是另一個(gè)話題)。因此,屬性值(或指向?qū)傩缘闹羔槪┚涂梢砸赃B續(xù)緩沖區(qū)存儲(chǔ)在內(nèi)存中,之間用固定的偏移量隔開。偏移量的長度簡單地根據(jù)屬性的類型確定,然而這在 JavaScript 中是不可能的,因?yàn)閷傩灶愋涂梢栽谶\(yùn)行時(shí)更改。
由于通過字典查找對(duì)象屬性在內(nèi)存中的位置非常低效,V8 采用了另一方法作為替代:隱藏類。隱藏類的原理類似于 Java 等語言中使用的固定對(duì)象布局(類),除了是在運(yùn)行時(shí)創(chuàng)建?,F(xiàn)在,讓我們來看看它們實(shí)際是什么樣的:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
當(dāng) new Point(1, 2) 調(diào)用發(fā)生,V8 將創(chuàng)建了一個(gè)名為 C0 的隱藏類。
現(xiàn)在 Point 還沒有定義任何屬性,所以 C0 是空的。
一旦第一條聲明 this.x = x 開始執(zhí)行(在 Point 函數(shù)內(nèi)),V8 將創(chuàng)建第二個(gè)基于 C0 的隱藏類 C1。C1 描述了在內(nèi)存中(相對(duì)于 point 對(duì)象)能找到屬性 x 的位置。在這個(gè)例子中,x 保存在偏移量為 0 的位置,這意味著在將內(nèi)存中的對(duì)象視作一個(gè)連續(xù)緩沖區(qū)時(shí),第一個(gè)偏移量對(duì)應(yīng)著 x。V8 還會(huì)通過一個(gè)“類轉(zhuǎn)換”更新 C0,以表明如果一個(gè)屬性 x 被添加到 point 對(duì)象中,隱藏類 C0 就會(huì)轉(zhuǎn)換成 C1。下面 point 對(duì)象的隱藏類現(xiàn)在變成了 C1。
每次添加一個(gè)新屬性到對(duì)象,舊隱藏類都會(huì)通過一個(gè)轉(zhuǎn)換路徑更新成一個(gè)新隱藏類。隱藏類轉(zhuǎn)換之所以如此重要是因?yàn)樗苁闺[藏類在以同樣方式創(chuàng)建的對(duì)象間共享。如果兩個(gè)對(duì)象共享同一個(gè)隱藏類并向它們添加相同的屬性,轉(zhuǎn)換可以確保它們獲得相同的隱藏類和所有與其相關(guān)的優(yōu)化代碼。
當(dāng) this.y = y 語句執(zhí)行時(shí)將會(huì)重復(fù)同樣的過程(同樣在 Point 函數(shù)內(nèi),this.x = x 之后)。
新的隱藏類 C2 將被創(chuàng)建,C1 發(fā)生類轉(zhuǎn)換表示如果向一個(gè) Point 對(duì)象添加屬性 y (已經(jīng)包含一個(gè)屬性 x),隱藏類應(yīng)該更新為 C2,并且 point 對(duì)象的隱藏類更新為 C2。
隱藏類轉(zhuǎn)換依賴向?qū)ο笏砑訉傩缘捻樞?。?qǐng)看下面的代碼片段:
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;
現(xiàn)在你可能會(huì)假設(shè) p1 和 p2 使用相同的隱藏類和轉(zhuǎn)換。實(shí)際則并非如此。對(duì)于 p1,先添加屬性 a 然后添加屬性 b。而對(duì)于 p2,先添加的屬性是 b 然后才是 a。因此,由于轉(zhuǎn)換路徑不同, p1 和 p2 最終將會(huì)產(chǎn)生不同的隱藏類。在這種情況下,最好在初始化動(dòng)態(tài)屬性時(shí)保持順序一致以便復(fù)用相同的隱藏類。
內(nèi)聯(lián)緩存V8 利用了另一項(xiàng)叫做內(nèi)聯(lián)緩存的技術(shù)來優(yōu)化動(dòng)態(tài)類型語言。內(nèi)聯(lián)緩存依賴于這樣一種觀察:同一方法的重復(fù)調(diào)用通常發(fā)生在同一類型的對(duì)象上。關(guān)于內(nèi)聯(lián)緩存的深入闡述在這里。
我們準(zhǔn)備介紹內(nèi)聯(lián)緩存的一般概念(以免你沒有時(shí)間查看上述的深入闡述)。
那么它的原理是什么?V8 維護(hù)著在最近的方法調(diào)用中作為參數(shù)傳入的對(duì)象類型的緩存,并利用這個(gè)信息假設(shè)未來會(huì)被當(dāng)做參數(shù)的對(duì)象的類型。如果 V8 能很好地假設(shè)出將要傳入方法的對(duì)象的類型,就能直接越過如何獲取對(duì)象屬性的計(jì)算過程,取而代之的是使用之前查找對(duì)象的隱藏類時(shí)存儲(chǔ)的信息。
那么隱藏類是如何與內(nèi)聯(lián)緩存關(guān)聯(lián)起來的?每當(dāng)某一對(duì)象調(diào)用方法時(shí),V8 必須執(zhí)行對(duì)此對(duì)象的隱藏類的查詢來確定訪問某個(gè)屬性的偏移量。當(dāng)對(duì)同一隱藏類成功調(diào)用過兩次同樣的方法后,V8 將省略對(duì)隱藏類的查詢而只將屬性偏移量添加到對(duì)象指針本身。對(duì)于那個(gè)方法未來所有的調(diào)用,V8 都假定隱藏類不改變,并利用之前查詢存儲(chǔ)的偏移量直接跳到某一屬性的內(nèi)存地址。這極大地提高了執(zhí)行速度。
內(nèi)聯(lián)緩存也是同類對(duì)象共享同一隱藏類如此重要的原因。如果你創(chuàng)建了擁有不同隱藏類的兩個(gè)同類對(duì)象(正如前面的例子),V8 就無法使用內(nèi)聯(lián)緩存,因?yàn)榧幢氵@兩個(gè)對(duì)象是相同的類型,但他們對(duì)應(yīng)的隱藏類為屬性指定了不同的偏移量。
這兩個(gè)對(duì)象基本相同,但 a、b 屬性的創(chuàng)建順序不同。編譯到機(jī)器代碼
一旦氮圖優(yōu)化好后,Crankshaft 會(huì)將它降為更低水平的表示,稱為 Lithium(注:鋰)。大多數(shù) Lithium 的實(shí)現(xiàn)依賴于特定架構(gòu)。寄存器分配發(fā)生在這個(gè)級(jí)別。
最終,Lithium 被編譯成機(jī)器代碼。隨后發(fā)生 OSR:堆棧上替換。在開始編譯和優(yōu)化明顯長時(shí)間運(yùn)行的方法前,我們可能會(huì)運(yùn)行它。V8 不會(huì)在再次開始執(zhí)行優(yōu)化版本時(shí)忘記那些緩慢的執(zhí)行。而是轉(zhuǎn)換我們所有的上下文(棧,寄存器)以便能在執(zhí)行中切換到優(yōu)化版本。這是個(gè)非常復(fù)雜的任務(wù),記住在其他的優(yōu)化中,V8 最先做了代碼內(nèi)聯(lián)。V8 不是唯一有這種能力的引擎。
還有種被稱為反優(yōu)化的安全措施能做反向轉(zhuǎn)換,回退到未優(yōu)化代碼,以防引擎做出的假設(shè)不再成立。
垃圾回收在垃圾回收方面,V8 采用傳統(tǒng)分代方法標(biāo)記和清掃來清理老的代。標(biāo)記階段會(huì)暫停 JavaScript 的執(zhí)行。為了控制垃圾回收的開銷并使執(zhí)行更加穩(wěn)定,V8 采用增量標(biāo)記:它不遍歷全部棧堆,而是嘗試標(biāo)記每一個(gè)可能的對(duì)象,它只遍歷棧堆的一部分,然后恢復(fù)正常執(zhí)行。下一次垃圾回收暫停會(huì)在之前棧堆的停止位置繼續(xù)。這可使正常執(zhí)行期間只發(fā)生相當(dāng)短的暫停。正如之前提到的,清理階段由多帶帶的線程處理。
Ignition 和 TurboFan隨著2017年初 V8 5.9版本的發(fā)布,一個(gè)新的執(zhí)行管道被引入。新的管道在實(shí)際的JavaScript 應(yīng)用中實(shí)現(xiàn)了更大的性能提升和的顯著的內(nèi)存節(jié)省。
新的執(zhí)行管道構(gòu)建在 V8 的解釋器 Ignition 和 V8 最新的優(yōu)化編譯器 TurboFan 之上。
你可以在這里查閱 V8 團(tuán)隊(duì)關(guān)于這個(gè)主題的博文。
自從 V8 5.9版本發(fā)布以來, V8 就不再在 JavaScript 執(zhí)行里使用 full-codegen 和 Crankshaft(自2010年來一直支撐著 V8 的技術(shù)),這是由于 V8 團(tuán)隊(duì)也在努力地跟上新的 JavaScript 語言特性的腳步和這些特性所需的優(yōu)化。
這意味著將來在整體上 V8 將擁有更加簡單和更易于維護(hù)的架構(gòu)。
這些提升僅僅是個(gè)開始。新的 Ignition 和 TurboFan 管道鋪墊了更遠(yuǎn)的優(yōu)化之路,將會(huì)推進(jìn) JavaScript 的性能并在接下來的幾年里縮小 V8 在 Chrome 和 Node.js 中的足跡。
最后,這里有幾條關(guān)于如何編寫更優(yōu)化的、更好的 JavaScript 代碼的建議和技巧。雖然你可以很容易地從上述的內(nèi)容中得到這些,為了方便還是把它們做了以下的總結(jié):
怎么編寫優(yōu)化的JavaScript對(duì)象屬性的順序:始終使用相同的順序初始化對(duì)象屬性,以便共享隱藏類和隨后的優(yōu)化代碼。
動(dòng)態(tài)屬性:在初始化完成之后添加對(duì)象動(dòng)態(tài)屬性會(huì)強(qiáng)制改變隱藏類并使之前的隱藏類已優(yōu)化的方法變慢。相反,在對(duì)象的構(gòu)造器里指定所有的屬性。
方法:重復(fù)執(zhí)行相同方法的代碼會(huì)比僅執(zhí)行一次許多不同的方法運(yùn)行的更快(由于內(nèi)聯(lián)緩存)。
數(shù)組:避免使用鍵值不遞增的稀疏數(shù)組。并非每個(gè)元素都存在的稀疏數(shù)組是一個(gè)哈希表。訪問稀疏數(shù)組的元素將會(huì)花費(fèi)更昂貴的開銷。此外,避免預(yù)先分配大數(shù)組。最好是按需要增加長度。最后,不要?jiǎng)h除數(shù)組中的元素。這會(huì)使數(shù)組變得稀疏。
帶標(biāo)記的值:V8 用32位字節(jié)表示對(duì)象和數(shù)字。其中使用了一個(gè)位來標(biāo)識(shí)是對(duì)象(標(biāo)識(shí)為1)或是整數(shù)(標(biāo)識(shí)為0),由于它們是31位的而被稱為 SMI(SMall Integer)。如果一個(gè)數(shù)值大小超過了31位可以表示的數(shù)字,V8 將會(huì)包裝它,將其轉(zhuǎn)換為一個(gè)雙字節(jié)類型值并創(chuàng)建一個(gè)新的對(duì)象存入其中。盡量使用31帶符號(hào)的數(shù)值避免 JS 對(duì)象的昂貴包裝操作。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/105009.html
摘要:引擎可以用標(biāo)準(zhǔn)解釋器或即時(shí)編譯器來實(shí)現(xiàn),即時(shí)編譯器以某種形式將代碼編譯為字節(jié)碼。這里的主要區(qū)別在于不生成字節(jié)碼或任何中間代碼。請(qǐng)注意,不使用中間字節(jié)碼表示法,不需要解釋器。這允許在正常執(zhí)行期間非常短的暫停。 本系列的第一篇文章重點(diǎn)介紹了引擎,運(yùn)行時(shí)和調(diào)用棧的概述。第二篇文章將深入V8的JavaScript引擎的內(nèi)部。我們還會(huì)提供一些關(guān)于如何編寫更好的JavaScript代碼的技巧。 概...
摘要:引擎可以是一個(gè)標(biāo)準(zhǔn)的解釋器,也可以是一個(gè)將編譯成某種形式的字節(jié)碼的即時(shí)編譯器。和其他引擎最主要的差別在于,不會(huì)生成任何字節(jié)碼或是中間代碼。不使用中間字節(jié)碼的表示方式,就沒有必要用解釋器了。 原文地址:https://blog.sessionstack.com... showImg(https://segmentfault.com/img/bVVwZ8?w=395&h=395); 數(shù)周之...
摘要:本章將會(huì)深入谷歌引擎的內(nèi)部結(jié)構(gòu)。一個(gè)引擎可以用標(biāo)準(zhǔn)解釋程序或者即時(shí)編譯器來實(shí)現(xiàn),即時(shí)編譯器即以某種形式把解釋為字節(jié)碼。引擎的由來引擎是由谷歌開源并以語言編寫。注意到?jīng)]有使用中間字節(jié)碼來表示,這樣就不需要解釋器了。 原文請(qǐng)查閱這里,略有刪減。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第二章。 本章將會(huì)深入谷歌 V8 引擎的內(nèi)部結(jié)構(gòu)。我們也會(huì)...
摘要:調(diào)用棧是單線程編程語言,意味著它只有單一的調(diào)用棧。調(diào)用棧是一種數(shù)據(jù)結(jié)構(gòu),基本記錄了程序運(yùn)行的位置。舉個(gè)例子,先來看如下所示的代碼當(dāng)引擎開始執(zhí)行這段代碼時(shí),調(diào)用棧將是空的。這正是拋出異常時(shí)棧追蹤的構(gòu)造過程這基本上就是異常拋出時(shí)調(diào)用棧的狀態(tài)。 原文 How JavaScript works: an overview of the engine, the runtime, and the c...
摘要:這些是中可用的最快屬性。通常來說我們將線性屬性存儲(chǔ)中存儲(chǔ)的屬性稱為。因此也支持所謂的屬性。整數(shù)索引屬性的處理和命名屬性的復(fù)雜性相同。 本文為譯文,原文地址:http://v8project.blogspot.com...,作者,@Camillo Bruni ,V8 JavaScript Engine Team Blog 在這篇博客中,我們想解釋 V8 如何在內(nèi)部處理 JavaScrip...
閱讀 982·2023-04-26 02:56
閱讀 9576·2021-11-23 09:51
閱讀 1888·2021-09-26 10:14
閱讀 2990·2019-08-29 13:09
閱讀 2161·2019-08-26 13:29
閱讀 578·2019-08-26 12:02
閱讀 3573·2019-08-26 10:42
閱讀 3012·2019-08-23 18:18