首頁  >  文章  >  web前端  >  認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

coldplay.xixi
coldplay.xixi轉載
2020-12-08 17:10:562847瀏覽

javascript專欄介紹深入V8引擎和編寫優化程式碼

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

相關免費學習推薦:javascript(影片)

概述

JavaScript引擎是執行JavaScript 程式碼的程式或解釋器。 JavaScript引擎可以實作為標準解釋器,或以某種形式將JavaScript編譯為字節碼的即時編譯器。

以為實作JavaScript引擎的流行專案的清單:

  • V8 — 開源,由Google 開發,以C 寫
  • Rhino — 由Mozilla 基金會管理,開源,完全用Java 開發
  • SpiderMonkey — 是第一個支援Netscape Navigator 的JavaScript 引擎,目前正供Firefox 使用
  • JavaScriptCore — 開源,以Nitro形式銷售,由蘋果為Safari開發
  • KJS — KDE 的引擎,最初由Harri Porten 為KDE 專案中的Konqueror 網頁瀏覽器開發
Chakra

(JScript9) — Internet Explorer


Chakra認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼 (JavaScript) — Microsoft Edge

##Nashorn

, 作為OpenJDK 的一部分,由Oracle Java 語言和工具組編寫

JerryScript
     —  物聯網的輕量級引擎
  • #為什麼要創建V8引擎?
由Google建構的V8引擎是開源的,使用c 編寫。這個引擎是在GoogleChrome中使用的,但是,與其他引擎不同的是 V8 也用於流行的 node.js。

  • V8最初是設計用來提升web瀏覽器中JavaScript執行的效能。為了獲得速度,V8 將 JavaScript 程式碼轉換成更有效率的機器碼,而不是使用解釋器。它透過實作 JIT (Just-In-Time) 編譯器將 JavaScript 程式碼編譯為執行時的機器碼,就像許多現代 JavaScript 引擎(如SpiderMonkey或Rhino (Mozilla)) 所做的那樣。這裡的主要區別是 V8 不產生字節碼或任何中間代碼。
  • V8 曾經有兩個編譯器
  • 在V8 的5.9 版本出來之前,V8 引擎使用了兩個編譯器:
  • full-codegen — 一個簡單和非常快的編譯器,產生簡單和相對較慢的機器碼。

Crankshaft — 一種更複雜(Just-In-Time)的最佳化編譯器,產生高度最佳化的程式碼。

V8 引擎也在內部使用多個執行緒:

主執行緒執行你所期望的操作:取得程式碼、編譯程式碼並執行它

#還有一個單獨的線程用於編譯,因此主線程可以在前者優化程式碼的同時繼續執行

一個Profiler 線程,它會告訴運行時我們花了很多時間,讓Crankshaft 可以優化它們認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

一些執行緒處理垃圾收集器

當第一次執行JavaScript 程式碼時,V8 利用full-codegen 編譯器,直接將解析的JavaScript 翻譯成機器碼而不進行任何轉換。這使得它可以非常快速地開始執行機器碼。請注意,V8 不使用中間字節碼,從而不需要解釋器。

當程式碼已經運行一段時間後,分析執行緒已經收集了足夠的資料來判斷應該優化哪個方法。

接下來,Crankshaft  從另一個執行緒開始優化。它將 JavaScript 抽象語法樹轉換為被稱為 Hydrogen 的高級靜態單一分配(SSA)表示,並嘗試優化 Hydrogen 圖,大多數最佳化都是在這個層級完成的。

###內聯程式碼######第一個最佳化是提前內聯盡可能多的程式碼。內聯是用被呼叫函數的主體取代呼叫點(呼叫函數的程式碼行)的過程。這個簡單的步驟允許下面的最佳化更有意義。 ###############隱藏類別######JavaScript是一種基於原型的語言:沒有使用複製過程建立類別和物件。 JavaScript也是一種動態程式語言,這意味著可以在實例化後輕鬆地在物件中新增或刪除屬性。 ######大多數JavaScript 解釋器使用類似字典的結構(基於雜湊函數)來儲存物件屬性值在記憶體中的位置,這種結構使得在JavaScript 中檢索屬性的值比在Java 或C#等非動態程式語言中的計算成本更高。 ######在Java中,所有物件屬性都是在編譯之前由固定物件佈局決定的,並且無法在執行時間動態新增或刪除(當然,C#具有動態類型,這是另一個主題)。 ###

因此,屬性值(或指向這些屬性的指標)可以作為連續緩衝區儲存在記憶體中,每個緩衝區之間具有固定偏移量, 可以根據屬性類型輕鬆確定偏移的長度,而在在執行時間可以更改屬性類型的JavaScript 中這是不可能的。

由於使用字典查找記憶體中物件屬性的位置效率非常低,因此 V8 使用了不同的方法:隱藏類別。隱藏類別與 Java 等語言中使用的固定物件(類別)的工作方式類似,只是它們是在執行時創建的。現在,讓我們看看他們實際的例子:

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

一旦「new Point(1,2)」 呼叫發生,V8 將建立一個名為「C0」 的隱藏類。

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

尚未為 Point 定義屬性,因此「C0」為空。

一旦第一個語句“this.x = x”被執行(在“Point”函數內),V8 將創建一個名為“C1” 的第二個隱藏類,它基於“C0” 。 「C1」描述了可以找到屬性 x 的記憶體中的位置(相對於物件指標)。

在這種情況下,“x”儲存在偏移0處,這意味著當將記憶體中的point 物件視為連續緩衝區時,第一個偏移將對應於屬性“x” 。 V8 也會使用 “類別轉換” 更新 “C0” ,該類別轉換​​指出如果將屬性 “x” 新增至 point 對象,則隱藏類別應從 “C0” 切換到 “C1”。下面的 point 物件的隱藏類別現在是「C1」。

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

每次將新屬性新增至物件時,舊的隱藏類別都會更新為指向新隱藏類別的轉換路徑。隱藏類別轉換非常重要,因為它們允許在以相同方式建立的物件之間共用隱藏類別。如果兩個物件共用一個隱藏類別並且相同屬性被添加到它們中,則轉換將確保兩個物件都接收相同的新隱藏類別以及隨其附帶的所有最佳化程式碼。

當語句 “this.y = y” 被執行時,會重複同樣的過程(在 “Point” 函數內部,“this.x = x”語句之後)。

一個名為“C2”的新隱藏類別會被創建,如果將一個屬性“y” 新增到一個Point 物件(已經包含屬性“x”),則類別轉換會新增到“C1” ,則隱藏類別應該變更為“C2”,point 物件的隱藏類別更新為“C2”。

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

隱藏類別轉換取決於將屬性新增到物件的順序。看看下面的程式碼片段:

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

現在,假設對於p1和p2,將使用相同的隱藏類別和轉換。那麼,對於“p1”,首先添加屬性“a”,然後添加屬性“b”。然而,“p2”首先分配“b”,然後是“a”。因此,由於不同的轉換路徑,「p1」和「p2」以不同的隱藏類別結束。在這種情況下,以相同的順序初始化動態屬性好得多,以便隱藏的類別可以被重複使用。

內嵌快取

V8利用了另一種最佳化動態類型語言的技術,稱為內嵌快取。內聯快取依賴於這樣一種觀察,即對相同方法的重複呼叫往往發生在同一類型的物件上。這裡可以找到對內聯快取的深入解釋。

接下來將討論內嵌快取的一般概念(如果您沒有時間透過上面的深入了解)。

那麼它是如何運作的呢? V8 維護了在最近的方法呼叫中作為參數傳遞的物件類型的緩存,並使用這些資訊預測將來作為參數傳遞的物件類型。如果 V8 能夠很好地預測傳遞給方法的物件的類型,它就可以繞過如何存取物件屬性的過程,而是使用從先前的查找到物件的隱藏類別的儲存資訊。

那麼隱藏類別和內聯快取的概念如何相關呢?無論何時在特定物件上呼叫方法時,V8 引擎都必須執行對該物件的隱藏類別的查找,以確定存取特定屬性的偏移量。在同一個隱藏類別的兩次成功的呼叫之後,V8 省略了隱藏類別的查找,並簡單地將該屬性的偏移量新增至物件指標本身。對於該方法的所有下一次調用,V8 引擎都假定隱藏的類別沒有更改,並使用從先前的查找儲存的偏移量直接跳到特定屬性的記憶體位址。這大大提高了執行速度。

內聯快取也是為什麼相同類型的物件共享隱藏類別非常重要的原因。如果你創建兩個相同類型和不同隱藏類別的物件(正如我們之前的例子中所做的那樣),V8將無法使用內聯緩存,因為即使這兩個物件屬於同一類型,它們對應的隱藏類別為其屬性分配不同的偏移量。

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

這兩個物件基本上相同,但是「a」和「b」屬性的建立順序不同。

編譯成機器碼

一旦 Hydrogen 圖表被最佳化,Crankshaft 將其降低到稱為 Lithium 的較低級表示。大部分的 Lithium 實作都是特定於架構的。寄存器分配往往發生在這個層級。

最後,Lithium 被編譯成機器碼。然後就是 OSR :on-stack replacement(堆疊替換)。在我們開始編譯和最佳化一個明確的長期運行的方法之前,我們可能會執行堆疊替換。 V8 不只是緩慢執行堆疊替換,並再次開始優化。相反,它會轉換我們擁有的所有上下文(堆疊,暫存器),以便在執行過程中切換到最佳化版本上。這是一個非常複雜的任務,考慮到除了其他最佳化之外,V8 最初還將程式碼內聯。 V8 不是唯一能夠做到的引擎。

有一種叫去優化的安全措施來進行相反的轉換,並在假設引擎無效的情況下傳回未最佳化的程式碼。

垃圾收集

對於垃圾收集,V8採用傳統的 mark-and-sweep 演算法 來清理舊一代。標記階段應該停止JavaScript執行。為了控制GC成本並使執行更穩定,V8使用增量標記:不是遍歷整個堆,嘗試標記每個可能的對象,它只是遍歷堆的一部分,然後恢復正常執行。下一個GC停止將從上一個堆行走停止的位置繼續,這允許在正常執行期間非常短暫的暫停,如前所述,掃描階段由單獨的線程處理。

如何寫最佳化的JavaScript

  1. 物件屬性的順序:總是以相同的順序實例化物件屬性,以便可以共用隱藏的類別和隨後最佳化的代碼。
  2. 動態屬性: 因為在實例化之後向物件添加屬性將強制執行隱藏的類別更改,並降低先前隱藏類別所優化的所有方法的執行速度,所以在其建構函數中分配所有物件的屬性。
  3. 方法:重複執行相同方法的程式碼將比僅執行一次的多個不同方法(由於內聯快取)的程式碼運行得更快。
  4. 陣列:避免稀疏數組,其中鍵值不是自增的數字,並沒有儲存所有元素的稀疏數組是哈希表。這種數組中的元素存取開銷較高。另外,盡量避免預先分配大數組。最好是按需成長。最後,不要刪除數組中的元素,這會使鍵值變得稀疏。
  5. 標記值:V8 使用 32 位元表示物件和數值。由於數值是 31 位元的,它使用了一位來區分它是物件(flag = 1)還是稱為 SMI(SMall Integer)整數(flag = 0)。那麼,如果一個數值大於 31 位,V8會將該數字裝箱,把它變成一個雙精度數,並建立一個新的物件來存放該數字。盡可能使用 31 位元有符號數字,以避免對 JS 物件的高開銷的裝箱操作。

Ignition and TurboFan

隨著2017稍早發布V8 5.9,引進了新的執行管道。這個新的管道在實際的JavaScript應用程式中實現了更大的效能提升和顯著節省記憶體。

新的執行流程是建立在 Ignition( V8 的解釋器)和 TurboFan( V8 的最新最佳化編譯器)之上的。

自從V8 5.9 版本問世以來,由於V8 團隊一直努力跟上新的JavaScript 語言特性以及這些特性所需的優化,V8 團隊已經不再使用full-codegen 和Crankshaft(自2010 年以來為V8 技術所服務)。

這意味著 V8 整體上將有更簡單、更容易維護的架構。

認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼

這些改進只是一個開始。新的Ignition和TurboFan管道為進一步優化鋪平了道路,這些優化將在未來幾年內提​​升JavaScript效能並縮小V8在Chrome和Node.js中的佔用空間。

相關免費學習推薦:php程式設計(影片)

#

以上是認識JavaScript是如何運作的,深入V8引擎和編寫優化程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除