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