摘要:本章將會深入谷歌引擎的內(nèi)部結(jié)構(gòu)。一個引擎可以用標準解釋程序或者即時編譯器來實現(xiàn),即時編譯器即以某種形式把解釋為字節(jié)碼。引擎的由來引擎是由谷歌開源并以語言編寫。注意到?jīng)]有使用中間字節(jié)碼來表示,這樣就不需要解釋器了。
原文請查閱這里,略有刪減。
本系列持續(xù)更新中,Github 地址請查閱這里。
這是 JavaScript 工作原理的第二章。
本章將會深入谷歌 V8 引擎的內(nèi)部結(jié)構(gòu)。我們也會為如何書寫更好的 JavaScript 代碼提供幾條小技巧-SessionStack 開發(fā)小組在構(gòu)建產(chǎn)品的時候所遵循的最佳實踐。
概述一個 JavaScript 引擎就是一個程序或者一個解釋程序,它運行 JavaScript 代碼。一個 JavaScript 引擎可以用標準解釋程序或者即時編譯器來實現(xiàn),即時編譯器即以某種形式把 JavaScript 解釋為字節(jié)碼。
以下是一系列實現(xiàn) JavaScript 引擎的熱門工程:
V8-由谷歌開源的以 C++ 語言編寫
Rhin-由 Mozilla 基金會主導(dǎo),開源的,完全使用 Java 開發(fā)。
SpiderMonkey-初代 JavaScript 引擎,由在之前由網(wǎng)景瀏覽器提供技術(shù)支持,現(xiàn)在由 Firefox 使用。
JavaScriptCore-開源,以 Nitro 的名稱來推廣,并由蘋果為 Safari 開發(fā)。
KJS-KDE 引擎,起先是由 Harri Porten 為 KDE 工程的 Konqueror 瀏覽器所開發(fā)。
Chakra (JScript9)-IE
Chakra (JavaScript)-Microsoft Edge
Nashorn-作為 OpenJDK 的一部分來開源,由 Oracle Java 語言和 Tool Group 編寫。
JerryScript-一款輕量級的物聯(lián)網(wǎng)引擎。
V8 引擎的由來V8 引擎是由谷歌開源并以 C++ 語言編寫。Google Chrome 內(nèi)置了這個引擎。而 V8 引擎不同于其它引擎的地方在于,它也被應(yīng)用于時下流行的 Node.js 運行時中。
起先 V8 是被設(shè)計用來優(yōu)化網(wǎng)頁瀏覽器中的 JavaScript 的運行性能。為了達到更快的執(zhí)行速度,V8 把 JavaScript 代碼轉(zhuǎn)化為更加高效的機器碼而不是使用解釋程序。它通過實現(xiàn)一個即時編譯器在運行階段把 JavaScript 代碼編譯為機器碼,就像諸如 SpiderMonkey or Rhino (Mozilla) 等許多現(xiàn)代 JavaScript 引擎所做的那樣。主要的區(qū)別在于 V8 不產(chǎn)生字節(jié)碼或者任何的中間碼。
V8 曾經(jīng)擁有兩個編譯器在 V8 5.9誕生(2017 年初) 之前,引擎擁有兩個編譯器:
full-codegen-一個簡單且快速的編譯器用來產(chǎn)出簡單且運行相對緩慢的機器碼。
Crankshaft-一個更復(fù)雜(即時)優(yōu)化的編譯器用來產(chǎn)生高效的代碼。
V8 引擎內(nèi)部也使用多個線程:
主線程做你所期望的事情-抓取你的代碼,編譯后執(zhí)行
有獨立的線程來編譯代碼,所以主線程可以保持執(zhí)行而前者正在優(yōu)化代碼
一個用于性能檢測的線程會告訴運行時我們在哪個方法上花了太多的時間,以便于讓 Crankshaft 來優(yōu)化這些代碼
有幾個線程用來處理垃圾回收器的清理工作。
當(dāng)?shù)谝淮螆?zhí)行 JavaScript 代碼的時候,V8 使用 full-codegen 直接把解析的 JavaScript 代碼解釋為機器碼,中間沒有任何轉(zhuǎn)換。這使得它一開始非??焖俚剡\行機器碼。注意到 V8 沒有使用中間字節(jié)碼來表示,這樣就不需要解釋器了。
當(dāng)你的代碼已經(jīng)執(zhí)行一段時間后,性能檢測器線程已經(jīng)收集了足夠多的數(shù)據(jù)來告訴 Crankshaft 哪個方法可以被優(yōu)化。
接下來,在另一個線程中開始進行 Crankshaft 代碼優(yōu)化。它把 JavaScript 語法抽象樹轉(zhuǎn)化為一個被稱為 Hydrogen 的高級靜態(tài)單賦值并且試著優(yōu)化這個 Hydrogen 圖表。大多數(shù)的代碼優(yōu)化是發(fā)生在這一層。
內(nèi)聯(lián)第一個優(yōu)化方法即是提前盡可能多地內(nèi)聯(lián)代碼。內(nèi)聯(lián)指的是把調(diào)用地址(函數(shù)被調(diào)用的那行代碼)置換為被調(diào)用函數(shù)的函數(shù)體的過程。這個簡單的步驟使得接下來的代碼優(yōu)化更有意義。
隱藏類JavaScript 是基于原型的語言:當(dāng)進行克隆的時候不會有創(chuàng)建類和對象。JavaScript 也是一門動態(tài)編程語言,這意味著在它實例化之后,可以任意地添加或者移除屬性。
大多數(shù)的 JavaScript 解釋器使用類字典的結(jié)構(gòu)(基于哈希函數(shù))在內(nèi)存中存儲對象屬性值的內(nèi)存地址(即對象的內(nèi)存地址)。這種結(jié)構(gòu)使得在 JavaScript 中獲取屬性值比諸如 Java 或者 C# 的非動態(tài)編程語言要更耗費時間。在 Java 中,所有的對象屬性都在編譯前由一個固定的對象布局所決定并且不能夠在運行時動態(tài)添加或者刪除(嗯, C# 擁有動態(tài)類型,這是另外一個話題)。因此,屬性值(指向這些屬性的指針)以連續(xù)的緩沖區(qū)的形式存儲在內(nèi)存之中,彼此之間有固定的位移。位移的長度可以基于屬性類型被簡單地計算出來,然而在 JavaScript 中這是不可能的,因為運行時可以改變屬性類型。
由于使用字典在內(nèi)存中尋找對象屬性的內(nèi)存地址是非常低效的,V8 轉(zhuǎn)而使用隱藏類。隱藏類工作原理和諸如 Java 語言中使用的固定對象布局(類)相似,除了它們是在運行時創(chuàng)建的以外?,F(xiàn)在,讓我們看看他們的樣子:
function Point(x, y) { this.x = x; this.y = y; } var p1 = new Point(1, 2);
一旦 "new Point(1,2)" 調(diào)用發(fā)生,V8 他創(chuàng)建一個叫做 "C0" 的隱藏類。
因為還沒有為類 Point 創(chuàng)建屬性,所以 "C0" 是空的。
一旦第一條語句 "this.x = x" 開始執(zhí)行(在 Point 函數(shù)中), V8 將會基于 "C0" 創(chuàng)建第二個隱藏類。"C1" 描述了可以找到 x 屬性的內(nèi)存地址(相對于對象指針)。本例中,"x" 存儲在位移 0 中,這意味著當(dāng)以內(nèi)存中連續(xù)的緩沖區(qū)來查看點對象的時候,位移起始處即和屬性 "x" 保持一致。V8 將會使用 "類轉(zhuǎn)換" 來更新 "C0","類轉(zhuǎn)換" 即表示屬性 "x" 是否被添加進點對象,隱藏類將會從 "C0" 轉(zhuǎn)為 "C1"。以下的點對象的隱藏類現(xiàn)在是 "C1"。
每當(dāng)對象添加新的屬性,使用轉(zhuǎn)換路徑來把舊的隱藏類更新為新的隱藏類。隱藏類轉(zhuǎn)換是重要的,因為它們使得以同樣方式創(chuàng)建的對象可以共享隱藏類。如果兩個對象共享一個隱藏類并且兩個對象添加了相同的屬性,轉(zhuǎn)換會保證兩個對象收到相同的新的隱藏類并且所有的優(yōu)化過的代碼都會包含這些新的隱藏類。
當(dāng)運行 "this.y = y" 語句的時候,會重復(fù)同樣的過程(還是在 Point 函數(shù)中,在 "this.x = x" 語句之后)。
一個被稱為 "C2" 的隱藏類被創(chuàng)造出來,一個類轉(zhuǎn)換被添加進 "C1" 中表示屬性 "y" 是否被添加進點對象(已經(jīng)擁有屬性 "x")之后隱藏會更改為 "C2",然后點對象的隱藏類會更新為 "C2"。
隱藏類轉(zhuǎn)換依賴于屬性被添加進對象的順序??慈缦碌拇a片段:
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)在,你會以為 p1 和 p2 會使用相同的隱藏類和類轉(zhuǎn)換。然而,對于 "p1",先添加屬性 "a" 然后再添加屬性 "b"。對于 "p2",先添加屬性 "b" 然后是 "a"。這樣,因為使用不同的轉(zhuǎn)換路徑,"p1" 和 "p2" 會使用不同的隱藏類。在這種情況下,更好的方法是以相同的順序初始化動態(tài)屬性以便于復(fù)用隱藏類。
內(nèi)聯(lián)緩存V8 利用了另一項優(yōu)化動態(tài)類型語言的技術(shù)叫做內(nèi)聯(lián)緩存。內(nèi)聯(lián)緩存依賴于對于同樣類型的對象的同樣方法的重復(fù)調(diào)用的觀察。這里有一份深入闡述內(nèi)聯(lián)緩存的文章。
我們將會接觸到內(nèi)聯(lián)緩存的大概概念(萬一你沒有時間去通讀以上的深入理解內(nèi)聯(lián)緩存的文章)。
它是如何工作的呢?V8 會維護一份傳入最近調(diào)用方法作為參數(shù)的對象類型的緩存,然后使用這份信息假設(shè)在未來某個時候這個對象類型將會被傳入這個方法。如果 V8 能夠很好地預(yù)判即將傳入方法的對象類型,它就可以繞過尋找如何訪問對象屬性的過程,代之以使用儲存的來自之前查找到的對象隱藏類的信息。
所以隱藏類的概念和內(nèi)聯(lián)緩存是如何聯(lián)系在一起的呢?每當(dāng)在一個指定的對象上調(diào)用方法的時候,V8 引擎不得不執(zhí)行查找對象隱藏類的操作,用來取得訪問指定屬性的位移。在兩次對于相同隱藏類的相同方法的成功調(diào)用之后,V8 忽略隱藏類的查找并且只是簡單地把屬性的位移添加給對象指針自身。在之后所有對這個方法的調(diào)用,V8 引擎假設(shè)隱藏類沒有改變,然后使用之前查找到的位移來直接跳轉(zhuǎn)到指定屬性的內(nèi)存地址。這極大地提升了代碼運行速度。
內(nèi)存緩存也是為什么同樣類型的對象共享隱藏類是如此重要的原因。當(dāng)你創(chuàng)建了兩個同樣類型的對象而使用不同的隱藏類(正如之前的例子所做的那樣),V8 將不可能使用內(nèi)存緩存,因為即使相同類型的兩個對象,他們對應(yīng)的隱藏類為他們的屬性分派不同的地址位移。
這兩個對象基本上是一樣的但是創(chuàng)建 "a" 和 "b" 的順序是不同的
編譯為機器碼一旦優(yōu)化了 Hydrogen 圖表,Crankshaft 會把它降級為低級的展現(xiàn)叫做 Lithium。大多數(shù) Lithium 的實現(xiàn)都是依賴于指定的架構(gòu)的。寄存器分配發(fā)生在這一層。
最后,Lithium 會被編譯為機器碼。之后其它被稱為 OSR 的事情發(fā)生了:堆棧替換。在開始編譯和優(yōu)化一個明顯的耗時的方法之前,過去極有可能去運行它。V8 不會忘記代碼執(zhí)行緩慢的地方,而再次使用優(yōu)化過的版本代碼。相反,它會轉(zhuǎn)換所有的上下文(堆棧,寄存器),這樣就可以在執(zhí)行過程中切換到優(yōu)化的版本代碼。這是一個復(fù)雜的任務(wù),你只需要記住的是,在其它優(yōu)化過程中,V8 會初始化內(nèi)聯(lián)代碼。V8 并不是唯一擁有這項能力的引擎。
這里有被稱為逆優(yōu)化的安全防護,以防止當(dāng)引擎所假設(shè)的事情沒有發(fā)生的時候,可以進行逆向轉(zhuǎn)換和把代碼反轉(zhuǎn)為未優(yōu)化的代碼。
垃圾回收V8 使用傳統(tǒng)的標記-清除技術(shù)來清理老舊的內(nèi)存以進行垃圾回收。標記階段會中止 JavaScript 的運行。為了控制垃圾回收的成本并且使得代碼執(zhí)行更加穩(wěn)定,V8 使用增量標記法:不遍歷整個內(nèi)存堆,試圖標記每個可能的對象,它只是遍歷一部分堆,然后重啟正常的代碼執(zhí)行。下一個垃圾回收點將會從上一個堆遍歷中止的地方開始執(zhí)行。這會在正常的代碼執(zhí)行過程中有一個非常短暫的間隙。之前提到過,清除階段是由多帶帶的線程處理的。
Ignition 和 TurboFan隨著 2017 早些時候 V8 5.9 版本的發(fā)布,帶來了一個新的執(zhí)行管道。新的管道獲得了更大的性能提升和在現(xiàn)實 JavaScript 程序中,顯著地節(jié)省了內(nèi)存。
新的執(zhí)行管道是建立在新的 V8 解釋器 Ignition 和 V8 最新的優(yōu)化編譯器 TurboFan 之上的。
你可以查看 V8 小組的博文。
自從 V8 5.9 版本發(fā)布以來,full-codegen 和 Crankshaft(V8 從 2010 開始使用至今) 不再被 V8 用來運行JavaScript,因為 V8 小組正努力跟上新的 JavaScript 語言功能以及為這些功能所做的優(yōu)化。
這意味著接下來整個 V8 將會更加精簡和更具可維護性。
網(wǎng)頁和 Node.js benchmarks 評分的提升
這些提升只是一個開始。新的 Ignition 和 TurboFan 管道為未來的優(yōu)化作鋪墊,它會在未來幾年內(nèi)提升 JavaScript 性能和縮減 Chrome 和 Node.js 中的 V8 痕跡。
最后,這里有一些如何寫出優(yōu)化良好的,更好的 JavaScript 代碼。你可以很容易地從以上的內(nèi)容中總結(jié)出來,然而,為了方便你,下面有份總結(jié):
如何寫優(yōu)化的 JavaScript 代碼對象屬性的順序:總是以相同的順序?qū)嵗愕膶ο髮傩?,這樣你的隱藏類及之后的優(yōu)化代碼都可以被共享。
動態(tài)屬性:實例化之后為對象添加屬性會致使為之前隱藏類優(yōu)化的方法變慢。相反,在對象構(gòu)造函數(shù)中賦值對象的所有屬性。
方法:重復(fù)執(zhí)行相同方法的代碼會比每次運行不同的方法的代碼更快(多虧了內(nèi)聯(lián)緩存)。
數(shù)列:避免使用鍵不是遞增數(shù)字的稀疏數(shù)列。稀疏數(shù)列中沒有包含每個元素的數(shù)列稱為一個哈希表。訪問該數(shù)列中的元素會更加耗時。同樣地,試著避免預(yù)先分配大型數(shù)組。最好是隨著你使用而遞增。最后,不要刪除數(shù)列中的元素。這會讓鍵稀疏。
標記值:V8 用 32 位來表示對象和數(shù)字。它使用一位來辨別是對象(flag=1)或者是被稱為 SMI(小整數(shù)) 的整數(shù)(flag=0),之所以是小整數(shù)是因為它是 31 位的。之后,如果一個數(shù)值比 31 位還要大,V8 將會裝箱數(shù)字,把它轉(zhuǎn)化為浮點數(shù)并且創(chuàng)建一個新的對象來存儲這個數(shù)字。盡可能試著使用 31 位有符號數(shù)字來避免創(chuàng)建 JS 對象的耗時裝箱操作。
本系列持續(xù)更新中,Github 地址請查閱這里。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/94803.html
摘要:本章會對語言引擎,運行時,調(diào)用棧做一個概述。調(diào)用棧只是一個單線程的編程語言,這意味著它只有一個調(diào)用棧。查看如下代碼當(dāng)引擎開始執(zhí)行這段代碼的時候,調(diào)用棧會被清空。之后,產(chǎn)生如下步驟調(diào)用棧中的每個入口被稱為堆棧結(jié)構(gòu)。 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原...
摘要:本章會對語言引擎,運行時,調(diào)用棧做一個概述。調(diào)用棧只是一個單線程的編程語言,這意味著它只有一個調(diào)用棧。查看如下代碼當(dāng)引擎開始執(zhí)行這段代碼的時候,調(diào)用棧會被清空。之后,產(chǎn)生如下步驟調(diào)用棧中的每個入口被稱為堆棧結(jié)構(gòu)。 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原...
摘要:事實是只是部分語言的不同表示法?;谶@些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關(guān)的額外無用功即解析函數(shù)。這里不解析函數(shù),該函數(shù)聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...
摘要:事實是只是部分語言的不同表示法?;谶@些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關(guān)的額外無用功即解析函數(shù)。這里不解析函數(shù),該函數(shù)聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...
閱讀 4001·2021-09-27 13:36
閱讀 4757·2021-09-22 15:12
閱讀 3184·2021-09-13 10:29
閱讀 1922·2021-09-10 10:50
閱讀 2483·2021-09-03 10:43
閱讀 616·2019-08-29 17:10
閱讀 509·2019-08-26 13:52
閱讀 3366·2019-08-23 14:37