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