幾個(gè)星期前,我們開始了一系列旨在深入挖掘 JavaScript 及其工作原理的系列:通過了解JavaScript的構(gòu)建模塊以及它們?nèi)绾喂餐l(fā)揮作用,你將能夠編寫更好的代碼和應(yīng)用程序。 本系列的第一篇文章重點(diǎn)介紹了引擎,運(yùn)行時(shí)和調(diào)用堆棧的概述。 第二篇文章將深入探討谷歌V8 JavaScript引擎的內(nèi)部原理。 我們還將提供一些關(guān)于如何編寫更好的JavaScript代碼的快速提示: 我們的SessionStack開發(fā)團(tuán)隊(duì)在構(gòu)建產(chǎn)品時(shí)所遵循的最佳實(shí)踐。 概覽JavaScript 引擎是執(zhí)行 JavaScript 代碼的程序或解釋器。 JavaScript 引擎可以實(shí)現(xiàn)為標(biāo)準(zhǔn)解釋器或即時(shí)編譯器,它以某種形式將 JavaScript 編譯為字節(jié)碼。 下面是一個(gè)JavaScript引擎實(shí)現(xiàn)的熱門項(xiàng)目列表:
為什么要開發(fā)V8引擎?由谷歌開發(fā)的V8引擎是用C ++編寫開源軟件。 此引擎在Google Chrome中使用。 但是,與其他引擎不同的是,流行的Node.js也把V8也作為運(yùn)行時(shí)環(huán)境使用。 V8最初是為了提高Web瀏覽器中 JavaScript 執(zhí)行的性能。 為了提高運(yùn)行速度,V8 將 JavaScript 代碼轉(zhuǎn)換為更高效的機(jī)器代碼,而不是使用解釋器運(yùn)行。 它通過實(shí)現(xiàn)JIT(即時(shí))編譯器將 JavaScript 代碼編譯成機(jī)器代碼,這一點(diǎn)與許多現(xiàn)代 JavaScript 引擎一樣,如 SpiderMonkey 或 Rhino(Mozilla)。 不過主要區(qū)別是V8不產(chǎn)生字節(jié)碼或任何中間代碼。 V8 曾經(jīng)有兩個(gè)編譯器在 V8 的 5.9 版本出現(xiàn)之前(2017年上半年發(fā)布),該引擎使用了兩個(gè)編譯器:
V8引擎還在內(nèi)部使用多個(gè)線程:
當(dāng)首次執(zhí)行 JavaScript 代碼時(shí),V8 會(huì)用 full-codegen直接將解析后的 JavaScript 代碼轉(zhuǎn)換為機(jī)器代碼而無需其它轉(zhuǎn)換。這使得它可以馬上開始執(zhí)行機(jī)器代碼。 請注意:V8不使用中間字節(jié)碼表示,因此無需解釋器。 當(dāng)代碼運(yùn)行一段時(shí)間之后,分析器線程已經(jīng)收集到了足夠的數(shù)據(jù),知道了應(yīng)該優(yōu)化哪個(gè)方法。 接下來,Crankshaft優(yōu)化從另一個(gè)線程開始。 它將 JavaScript 抽象語法樹轉(zhuǎn)換成名為 Hydrogen的高級(jí)靜態(tài)單分配(SSA:static single-assignment)表示,并嘗試優(yōu)化 Hydrogen graph。 大多數(shù)優(yōu)化都是在這個(gè)級(jí)別完成的。 內(nèi)聯(lián)第一個(gè)優(yōu)化是提前內(nèi)聯(lián)盡可能多的代碼。 內(nèi)聯(lián)是用被調(diào)函數(shù)的函數(shù)體替換調(diào)用點(diǎn)(調(diào)用函數(shù)的代碼行)的過程。 這個(gè)簡單的步驟使后面的優(yōu)化更有意義。 隱藏類JavaScript是一種基于原型的語言:沒有類,使用克隆過程創(chuàng)建對象。 JavaScript也是一種動(dòng)態(tài)編程語言,這意味著可以在實(shí)例化后可以輕松地在對象中添加或刪除屬性。 大多數(shù)JavaScript解釋器使用類似字典的結(jié)構(gòu)(基于散列函數(shù))在內(nèi)存中存儲(chǔ)對象屬性值。 這種結(jié)構(gòu)使得在JavaScript中檢索屬性值的計(jì)算成本比在 Java 或 C# 等非動(dòng)態(tài)編程語言中更高。 在Java中,所有對象屬性都是在編譯之前由固定對象布局確定的,并且無法在運(yùn)行時(shí)動(dòng)態(tài)添加或刪除(好吧,C# 具有動(dòng)態(tài)類型,不過這是另一個(gè)話題)。 這樣一來,屬性值(或指向這些屬性的指針)可以作為連續(xù)緩沖區(qū)存儲(chǔ)在存儲(chǔ)器中,每個(gè)緩沖區(qū)之間具有固定偏移量,可以根據(jù)屬性類型輕松確定偏移的長度。而對于在運(yùn)行時(shí)可以更改屬性類型的 JavaScript,這是不可能做到的。 由于使用字典查找對象屬性在內(nèi)存中的位置效率非常低,因此V8使用不同的方法:隱藏類。 隱藏類的工作方式類似于 Java 等語言中使用的固定對象布局(類),除非它們是在運(yùn)行時(shí)創(chuàng)建的。 現(xiàn)在,讓我們看看它們實(shí)際上是什么樣的: 一旦 此時(shí)尚未為Point定義任何屬性,因此 一旦第一個(gè)語句 每次將新屬性添加到對象時(shí),舊的隱藏類都會(huì)更新為指向新隱藏類的轉(zhuǎn)換路徑。 隱藏類轉(zhuǎn)換非常重要,因?yàn)樗鼈冊试S在以相同方式創(chuàng)建的對象之間共享隱藏類。 如果兩個(gè)對象共享一個(gè)隱藏類,并且同一屬性被添加到它們之中,那么轉(zhuǎn)換將確保兩個(gè)對象都能夠接收到相同的新隱藏類和隨之附帶的所有優(yōu)化代碼。 在執(zhí)行語句 創(chuàng)建一個(gè)名為 隱藏類的轉(zhuǎn)換取決于屬性添加到對象的順序。 看下面的代碼片段: 看到上面的代碼,你會(huì)認(rèn)為對于p1和p2,將使用相同的隱藏類和轉(zhuǎn)換。 實(shí)際上不是這樣的。 對于 內(nèi)聯(lián)緩存V8 還使用了另一種技術(shù)來優(yōu)化動(dòng)態(tài)類型語言,被稱為內(nèi)聯(lián)緩存。 內(nèi)聯(lián)緩存依賴于觀察到的一種現(xiàn)象,那就是相同方法總是會(huì)被同一類型的對象的重復(fù)調(diào)用。 可以在這里找到對內(nèi)聯(lián)緩存的深入解釋 (https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches)。 下面我們將討論內(nèi)聯(lián)緩存的一般概念(如果你沒有時(shí)間仔細(xì)閱讀上面的深入解釋的話)。 那么它是怎樣工作的呢? V8 維護(hù)一個(gè)在最近的方法調(diào)用中作為參數(shù)傳遞的對象類型的緩存,并以此信息來推測將來作為參數(shù)傳遞的對象類型。如果V8能夠正確的推測出對傳遞給方法的對象類型,那么它就可以跳過確定如何訪問對象屬性的這一個(gè)步驟,這樣就可以使用之前查找過的信息確定對象的隱藏類。 那么隱藏類和內(nèi)聯(lián)緩存這兩個(gè)概念的關(guān)聯(lián)是什么呢?每當(dāng)在特定對象上調(diào)用方法時(shí),V8 引擎必須找到該對象的隱藏類,才能確定訪問特定屬性的偏移量。當(dāng)同一方法兩次成功調(diào)用到同一個(gè)隱藏類之后,V8會(huì)省略對隱藏類的查找,直接將屬性的偏移量添加到對象指針本身。對于該方法的所有將來的調(diào)用,V8引擎假設(shè)隱藏類并未更改,并且使用之前查找到并存儲(chǔ)的偏移量直接跳轉(zhuǎn)到特定屬性的內(nèi)存地址。這就大大提高了執(zhí)行速度。 內(nèi)聯(lián)緩存也是相同類型的對象共享隱藏類的重要原因。如果你要?jiǎng)?chuàng)建兩個(gè)類型相同但是隱藏類不同的對象(正如我們之前的例子中所做的那樣)的話,V8將無法使用內(nèi)聯(lián)緩存,因?yàn)榧词惯@兩個(gè)對象屬于同一類型,但是它們相對應(yīng)的隱藏類為其屬性分配的偏移量很有可能是不同的。 a 和b 兩個(gè)屬性是按照不同順序創(chuàng)建的。這兩個(gè)對象基本相同,但 編譯為機(jī)器代碼Hydrogen graph優(yōu)化后,Crankshaft 會(huì)將其降低到被稱為 Lithium 的低級(jí)別表示。大多數(shù) Lithium 實(shí)現(xiàn)都是特定于體系結(jié)構(gòu)的。寄存器分配發(fā)生在這一級(jí)別。 最后,Lithium 被編譯成機(jī)器代碼。然后發(fā)生了一些被稱為 OSR 的事:棧替換(on-stack replacement)。當(dāng)一個(gè)顯然會(huì)長時(shí)間運(yùn)行的方法在我們開始編譯和優(yōu)化之前,它可能已經(jīng)在運(yùn)行。 V8 在重新啟動(dòng)優(yōu)化版本之前并會(huì)任由這些代碼緩慢的執(zhí)行。相反,它將轉(zhuǎn)換我們擁有的所有上下文(堆棧,寄存器),以便可以在執(zhí)行過程中切換到優(yōu)化版本。這是一項(xiàng)非常復(fù)雜的任務(wù),考慮到其他優(yōu)化,V8在一開始就已經(jīng)內(nèi)聯(lián)了代碼。 V8并不是唯一能夠做到這一點(diǎn)的引擎。 有一種被稱為去優(yōu)化的保護(hù)措施可以進(jìn)行相反的轉(zhuǎn)換,如果引擎作出的假設(shè)不再成立,則恢復(fù)到非優(yōu)化代碼。 垃圾收集對于垃圾收集,V8采用傳統(tǒng)的標(biāo)記和掃描方式來清理老生代。標(biāo)記階段應(yīng)該停止JavaScript執(zhí)行。為了控制GC成本并使執(zhí)行更加穩(wěn)定,V8使用了增量標(biāo)記:不是遍歷整個(gè)堆的同時(shí)嘗試標(biāo)記每個(gè)可能的對象,它只是遍歷堆的一部分,然后恢復(fù)正常執(zhí)行。 下一次GC將從上一次堆遍歷停止的位置繼續(xù)。這樣會(huì)在正常執(zhí)行期間只有非常短暫的暫停。 如前文所述,掃描階段由單獨(dú)的線程進(jìn)行處理。 Ignition and TurboFan2017年早些時(shí)候發(fā)布的V8 5.9,引入了新的執(zhí)行管道。 事實(shí)證明,這個(gè)新的管道實(shí)現(xiàn)了更高的性能提升,并顯著的節(jié)省了內(nèi)存開銷。 新的執(zhí)行管道建立在 Ignition (https://github.com/v8/v8/wiki/Interpreter)、V8的解釋器和TurboFan(V8的最新優(yōu)化編譯器)之上。 你可以查看V8團(tuán)隊(duì)關(guān)于該主題的博客文章 (https://v8project./2017/05/launching-ignition-and-turbofan.html)。 自從V8的 5.9 版本問世以來,V8已經(jīng)不再使用 full-codegen 和 Crankshaft(自2010年以來為V8提供服務(wù)的技術(shù))用于JavaScript執(zhí)行,因?yàn)閂8團(tuán)隊(duì)一直在努力跟上新的JavaScript語言功能,并且這些功能需要優(yōu)化。 這意味著整體V8將會(huì)具有更簡單,更易維護(hù)的架構(gòu)。 這些改進(jìn)只是一個(gè)開始。 新的Ignition和TurboFan管道為進(jìn)一步優(yōu)化鋪平了道路,這些優(yōu)化將在未來幾年內(nèi)提升JavaScript性能,并減少V8在Chrome和Node.js中所占用的空間。 最后,有一些關(guān)于如何編寫良好優(yōu)化的JavaScript的技巧和竅門。 你可以從上面的內(nèi)容輕松地推導(dǎo)出這些內(nèi)容,下面是一個(gè)簡要的總結(jié): 如何編寫優(yōu)化的JavaScript代碼
我們在為 SessionStack 編寫高度優(yōu)化的 JavaScript 代碼時(shí)一直遵循這些最佳實(shí)踐。 原因是一旦把 SessionStack 集成到Web應(yīng)用的生產(chǎn)環(huán)境中,它就會(huì)開始記錄所有內(nèi)容:所有DOM更改、用戶交互、JavaScript異常、堆棧跟蹤、失敗的網(wǎng)絡(luò)請求和調(diào)試消息。 通過SessionStack,你可以將網(wǎng)絡(luò)應(yīng)用中的問題重現(xiàn),并查看發(fā)生的所有事情,同時(shí)對你的Web應(yīng)用沒有性能影響。 有一個(gè)免費(fèi)的工具,不需要支付任何費(fèi)用。 現(xiàn)在就可以試試(https://www./solutions/developers/?utm_source=medium&utm_medium=blog&utm_content=Post-1-overview-getStarted)。 |
|