V8 架構優勢總結


1、什么是V8引擎?

V8使用C++開發,並在谷歌瀏覽器中使用。

在運行JavaScript之前,相比其它的JavaScript的引擎轉換成字節碼解釋執行,V8將其編譯成原生機器碼IA-32x86-64ARM, or MIPS CPUs)

並且使用如內聯緩存(inline caching)方法來提高性能。

有了這些改進,JavaScript程序在V8引擎下的運行速度媲美二進制程序。

v8版本發布歷程

2017-4-27 發布v5.9

2018-8-7 發布v6.9

2019-8-13 發布v7.7

2019-9-27 發布v7.8

2019-11-20 發布v7.9

2019-12-18 發布v8.0  

在 V8 的 5.9 版本出來之前使用兩個編譯器:

full-codegen - 一個簡單而且速度非常快的編譯器,可以生成簡單且相對較慢的機器代碼。

Crankshaft(曲柄軸)  - 一種更復雜(Just-In-Time)的優化編譯器,生成高度優化的代碼。

V8 內部使用多個線程:

主線程,完成您期望的任務:獲取代碼,編譯並執行它

編譯線程,一個線程用於編譯,以便主線程可以繼續執行,而前者正在優化代碼

分析線程,一個Profiler 線程用於分析,它會告訴運行時花費很多時間,讓 Crankshaft 優化它們

垃圾收集器,其他線程用於處理,垃圾收集

 

2、其他JS解析引擎有哪些?

V8,Google開發,C++編寫,應用在Node和Chrome

SpiderMonkey(蜘蛛猴) 第一個JavaScript引擎,網景開發,應用在Firefox

JavaScriptCore 蘋果公司的Safari

Chakra (JScript9) Edge瀏覽器

Chakra (JavaScript) IE9-IE11

 

3、V8在新版本中的優化有哪些?

3.1、內聯代碼
 
類型轉化
這個優化移除了打包和拆包的處理,類型轉換會執行很多指令。如果代碼在操作整數,並且沒有做類型轉換,那么它會跑的很快。
 
類型判斷
內聯緩存會在內聯代碼階段起到重要的作用,提供類型判斷。
如果希望一個變量是整數,但是被修改成了其它類型,需要一次重新編譯。
 
其它優化
loop-invariant (將循環內不變代碼移到外面,減少每次循環執行代碼量)
code motion          (不被執行的代碼移除,減輕了V8 的額外負擔)
 
3.2、隱藏類
 
JavaScript 是基於原型的語言:沒有類、沒有對象,使用克隆創建。 JavaScript 是一種動態語言,屬性可以在實例化之后添加或移除,像Java肯定是不行的!
 
大多數 JavaScript 解釋器使用字典結構(基於散列函數)來存儲對象屬性值在內存中的位置。
 
這種結構使得在JavaScript 中檢索屬性的值比在 Java 或 C# 非動態編程語言中的計算量要大得多,檢索屬性值費時。
 
(在 Java 中,所有的對象屬性編譯都由一個固定的對象決定,不能在運行時動態添加或刪除。
因此,屬性的值(或指向這些屬性的指針)可以作為連續的緩沖區被存儲在內存中,每個值之間有一個固定的偏移量根據屬性類型確定偏移量。)
 
在 Javascript 中可以動態的在實例上添加刪除屬性,且沒有固定類型,不適合用連續的緩沖區存儲也沒有類型來確定偏移量,V8用字典存儲解決了這個問題,但計算量很大。
 
由於使用字典查找內存中對象屬性的位置效率很低,因此 V8 使用了隱藏類的方法
隱藏類與 Java 中使用的 固定類 的工作方式相似,區別是隱藏類是在運行時創建的。
function Point(x,y){
  this.x = x;
  this.y = y;
}
var p1 = new Point(1,2)
//此處在執行 new Point()時,v8開始創建一個隱藏類 C0

分析隱藏類執行過程:

第一句 “this.x = x” 被執行時V8 將創建第二個隱藏類,名為“C1”,基於“C0”。 “C1”描述了可以找到屬性x在內存中的位置(相對於對象指針)。

在這種情況下,“x”被存儲在0處,因此在內存中將對象看作一段連續存儲空間時,第一個地址將對應屬性“x”。

V8 也會用“class transition”來更新“C0”,如果一個屬性“x”被添加到一個對象時,隱藏類應該從“C0”切換到“C1”。

下面的點對象的隱藏類現在是“C1”。

每當一個新的屬性被添加到一個對象時,舊的隱藏類會被更新到新的隱藏類的轉換路徑,稱為隱藏類的轉換。

隱藏類的轉換非常重要,因為它允許隱藏類以相同方式創建的對象之間共享。

增加屬性y隱藏類轉換為C2.

隱藏類轉換取決於給對象賦值屬性的順序。

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;

在這種情況下,創建兩個隱藏類p1\p2,p1和p2的賦值順序不同,以不同的隱藏類別結束,這種情況隱藏類無法被復用,如果賦值順序相同則可以復用隱藏類。

 

3.3、內聯緩存

內聯緩存優化 動態類型語言。

內聯緩存依賴於觀察,重復調用的觀察,相同類型對象上相同方法的重復調用。

V8 維護一個在最近的方法調用中作為參數傳遞的對象類型的緩存,並使用這些信息來預測將來作為參數傳遞的對象的類型。

如果V8能夠很好地假定傳遞給方法的對象類型,那么它可以繞過如何訪問對象的屬性的過程,而是將之前查找到的信息用於對象的隱藏類。

無論何時在特定對象上調用方法時,V8 引擎都必須執行對該對象隱藏類的查找。

以確定訪問特定屬性的偏移量。

在同一個隱藏類的兩次成功的調用之后,V8 省略了隱藏類的查找,並簡單地將該屬性的偏移量添加到對象指針本身。

對於該方法的所有下一次調用,V8 引擎都假定隱藏的類沒有更改

並使用從以前的查找存儲的偏移量直接跳轉到特定屬性的內存地址,提高了執行速度。

內聯緩存也是為什么相同類型的對象可以共享隱藏類非常重要的原因。

如果你創建了兩個相同類型的對象和不同的隱藏類(就像我們之前的例子中那樣),V8 將不能使用內聯緩存

使用內聯緩存條件:即使兩個對象是相同的類型,它們相應的隱藏類為其屬性分配不同的偏移量;要求不僅要類型相同、還要順序相同。

 
3.4、編譯為機器碼

當 Hydrogen 圖被優化,Crankshaft 將其降低到 Lithium 低級表示。

大部分 Lithium 實現都是特定於架構,寄存器分配發生在這個級別。

最后Lithium 被編譯成機器碼。

然后是 OSR :on-stack replacement(堆棧替換)

在我們開始編譯和優化一個明確的長期運行的方法之前,我們可能會運行堆棧替換。

V8 不只是緩慢執行堆棧替換,並再次開始優化。相反,它會轉換我們擁有的所有上下文(堆棧,寄存器)

以便執行過程中切換到優化版本。

這是一個非常復雜的任務。

有一種相反的變換,被稱為去優化的保護措施做出相反變換,並假設引擎優化無效,還原成非優化代碼。

 

3.5、垃圾回收 

對於垃圾收集,V8 采用了傳統的分代式掃描方式來清理老一代

標記階段應該停止 JavaScript 的執行。為了控制 GC(garbage collection) 成本並使執行更加穩定,V8 使用了漸進式標記:而不是走遍整個堆內容,試圖標記每一個可能的對象。它只走一部分堆內容,然后恢復正常執行。下一個 GC 將從先前堆走過的地方繼續執行。這允許在正常執行期間非常短的暫停。如前所述,掃描階段由不同的線程處理。

新的 Ignition 和 TurboFan 管道為進一步的優化鋪平了道路,將提高 JavaScript 性能,縮小 V8 在 Chrome 和 Node.js 中的占用空間。
取代了full-codegen 和 Crankshaft
 
 
3.6、如何編寫更有效的 JavaScript

對象屬性的順序:始終以相同的順序實例化對象屬性,以便共享的隱藏類和隨后優化的代碼可以共享之。

動態屬性:在實例化之后向對象添加屬性將強制執行隱藏的類更改,並降低之前隱藏類所優化的所有方法的執行速度。在構造函數中分配所有對象屬性更好。

方法:重復執行相同方法的代碼將比僅執行一次的多個不同方法(由於內聯緩存)的代碼運行得更快,所以應該盡量復用函數。

數組:避免稀疏數組,其中鍵值不是自增的數字。並沒有存儲所有元素的稀疏數組是哈希表。這種數組中的元素訪問開銷較高。另外,盡量避免預分配大數組。最好是按需增長。最后,不要刪除數組中的元素。這會使鍵值變得稀疏。

標記值:V8 使用 32 位表示對象和數值。由於數值是 31 位的,它使用了一位來區分它是一個對象(flag = 1)還是一個稱為 SMI(SMall Integer)整數(flag = 0)。那么,如果一個數值大於 31 位,V8會將該數字裝箱,把它變成一個雙精度數,並創建一個新的對象來存放該數字。盡可能使用 31 位有符號數字,以避免對 JS 對象的高開銷的裝箱操作。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM