【譯】V8 引擎怎樣對屬性進行快速訪問

V8 引擎怎樣對屬性進行快速訪問

在這篇文章中我將要解釋 V8 引擎內部是如何處理 JavaScript 屬性的。從 JavaScript 的角度來看,屬性們區別並不大,JavaScript 對象表現形式更像是字典,字符串做爲鍵,任意對象做爲值。ECMAScript 語言規範 中,對象的數字索引和其餘類型索引在規範中沒有明確區分,可是在 V8 引擎內部卻不是這樣的。除此以外,不一樣屬性的行爲基本相同,和他們可不能夠進行整數索引沒有關係。html

然而在 V8 引擎中屬性的不一樣表現形式確實會對性能和內存有影響,在這篇文章中咱們來解析 V8 引擎是如何可以在動態添加屬性時進行快速的屬性訪問的,理解屬性是如何工做的,以解釋 V8 引擎是如何的優化,(例如 內聯緩存 )。前端

這篇文章解釋了處理整數索引屬性和命名屬性的不一樣之處,以後咱們展現了 V8 中是如何爲了提供一個快速的方式定義一個對象的模型在添加一個命名屬性時使用 HiddenClasses。而後,咱們將繼續深刻了解如何根據使用狀況進行屬性名的命名優化,以便可以快速訪問或者快速修改。在最後一節中,咱們介紹 V8 如何處理整數索引屬性或數組索引的詳細信息。 react

命名屬性和元素

讓咱們從分析一個很是簡單的對象開始,好比:{a: "foo", b: "bar"}。這個對象有兩個命名屬性,"a" 和 "b"。它沒有使用任何的整數索引做爲屬性名。咱們也可使用索引訪問屬性,特別是對象爲數組的狀況。例如,數組 ["foo", "bar"] 有兩個可使用數組索引的屬性:索引爲 0 的值是 "foo",索引爲 1 的值是 "bar"android

這是 V8 通常處理屬性的第一個主要區別。ios

下圖顯示了一個 JavaScript 的基本對象在內存中的樣子。git

元素和屬性存儲在兩個獨立的數據結構中,這使得使用不一樣的模式添加和訪問屬性和元素將會更加高效。github

元素主要用於各類 Array.prototype methods 例如 popslice。考慮到這些函數是在連續範圍存儲區域內訪問屬性的,V8 引擎內部大部分狀況下也將他們表示爲簡單的數組。稍後咱們將解釋如何使用一個稀疏的基於字典的表示來節省內存。編程

命名屬性的存儲相似於稀疏數組的存儲。然而,與元素不一樣,咱們不能簡單的使用鍵推斷其在屬性數組中的位置,咱們須要一些額外的元數據。在 V8 中,每個 JavaScript 對象都有一個相關聯的 HiddenClass。這個 HiddenClass 存儲了一個對象的模型信息,在其餘方面,有一個從屬性名到屬性索引映射。咱們有時使用一個字典來代替簡單的數組。咱們專門會在一個章節中更詳細地解釋這一點。後端

本節重點:數組

  • 數組索引屬性存儲在單獨的元素存儲區中。
  • 命名屬性存儲在屬性存儲區中。
  • 元素和屬性能夠是數組或字典。
  • 每一個 JavaScript 對象有一個和對象的模型相關聯的 HiddenClass

HiddenClasses 和描述符數組

在介紹了元素和命名屬性的大體區別以後,咱們須要來看一下 HiddenClasses 在 V8 中是怎麼工做的。HiddenClass 存儲了一個對象的元數據,包括對象和對象引用原型的數量。HiddenClasses 在典型的面向對象的編程語言的概念中和「類」相似。然而,在像 JavaScript 這樣的基於原型的編程語言中,通常不可能預先知道類。所以,在這種狀況下,在 V8 引擎中,HiddenClasses 建立和更新屬性的動態變化。HiddenClasses 做爲一個對象模型的標識,而且是 V8 引擎優化編譯器和內聯緩存的一個很是重要的因素。經過 HiddenClass 能夠保持一個兼容的對象結構,這樣的話實例能夠直接使用內聯的屬性。

讓咱們來看一下 HiddenClass 的重點

在 V8 中,JavaScript 對象的第一部分就是指向 HiddenClass。(實際上,V8 中的任何對象都在堆中而且受垃圾回收器管理。)在屬性方面,最重要的信息是第三段區域,它存儲屬性的數量,以及一個指向描述符數組的指針。描述符數組包含有關命名屬性的信息,如名稱自己和存儲值的位置。注意,咱們不在這裏跟蹤整數索引屬性,所以描述符數組中沒有整數索引的條目。

關於 HiddenClasses 的基本假設是對象具備相同的結構,例如,相同的順序對應相同的屬性,共用相同的 HiddenClass。當咱們給一個對象添加一個屬性的時候咱們使用不一樣的 HiddenClass 實現。在下面的例子中,咱們從一個空對象開始而且添加三個命名屬性。

每次加入一個新屬性時,對象的 HiddenClass 就會改變,在 V8 引擎的後臺會建立一個將 HiddenClass 鏈接在一塊兒的轉移樹。V8 引擎就知道你添加的 HiddenClass 是哪個了,例如,屬性 「a」 添加到一個空對象中,若是你以相同的順序添加相同的屬性,這個轉化樹會使用相同的 HiddenClass。下面的示例代表,即便在二者之間添加簡單的索引屬性,咱們也將遵循相同的轉換樹。

本節重點:

  • 結構相同的對象(相同的順序對於相同的屬性)有相同的 HiddenClasses。
  • 默認狀況下,每添加一個新的命名屬性將產生了一個新的 HiddenClasses。
  • 增長數組索引屬性並不創造新 HiddenClasses。

三種不一樣的命名屬性

在概述了 V8 引擎是如何使用 HiddenClasses 來追蹤對象的模型以後,咱們來看一下這些屬性其實是如何儲存的。正如上面介紹所介紹的,有兩種基本屬性:命名屬性和索引屬性。如下部分是命名屬性:

一個簡單的對象,例如 {a: 1, b: 2} 在 V8 引擎的內部有多種表現形式,雖然 JavaScript 對象或多或少的和外部的字典類似,V8 引擎仍然試圖避免和字典相似由於他們妨礙某些優化,例如 內聯緩存,咱們將在一篇單獨的文章中解釋。

In-object 屬性和通常屬性: V8 引擎支持直接儲存在所謂的 In-object 的屬性。這些是 V8 引擎中可用的最快速的屬性,由於他們能夠直接訪問。In-object 屬性的數量是由對象的初始大小決定的。若是在對象中添加超出存儲空間的屬性,那麼他們會儲存在屬性存儲區中。屬性存儲多了一層間接尋址但這是獨立的區域。

快屬性 VS 慢屬性: 下一個重要的區別來自於快屬性和慢屬性。一般,咱們將存儲在線性屬性存儲區域的屬性稱爲快屬性。快屬性僅經過屬性存儲區的索引訪問,爲了在屬性存儲區的實際位置獲得屬性的名字,咱們必須經過在 HiddenClass 中的描述符數組。

然而,從一個對象中添加或刪除多個屬性,會爲了保持描述符數組和 HiddenClasses 而產生大量的時間和內存的開銷。所以,V8 引擎也支持所謂的慢屬性,一個有慢屬性的對象有一個自包含的字典做爲屬性存儲區。全部的屬性元數據都再也不存儲在 HiddenClass 的描述符數組而是直接在屬性字典。所以,屬性能夠添加和刪除不更新的 HiddenClass。因爲內聯緩存不使用字典屬性,後者一般比快速屬性慢。

本節重點:

  1. 有三種不一樣的命名屬性類型:對象、快字典和慢字典。
  • 在對象屬性中直接存儲在對象自己上,並提供最快的訪問速度。
  • 快屬性存儲在屬性存儲區,全部的元數據存儲在 HiddenClass 的描述符數組中。
  • 慢屬性存儲在自身的屬性字典中,元數據再也不存儲於 HiddenClass。
  1. 慢屬性容許高效的屬性刪除和添加,但訪問速度比其餘兩種類型慢。

元素或數組索引屬性

到目前爲止,咱們已經瞭解了命名屬性,在研究的過程當中忽略數組中經常使用的整數索引屬性。處理整數索引屬性並不比命名屬性簡單。雖然全部的索引屬性老是單獨存放在元素存儲中,可是有 20 種不一樣類型的元素!

元素是連續的的仍是有缺省的: V8 引擎的第一個主要區別是元素在存儲區是連續的仍是有缺省的。若是刪除索引元素,或者在不定義索引元素的狀況下,就會在存儲區中有一個缺省。一個簡單的例子是 [1,,3],第二個位置缺省。下面的例子說明了這個問題:

const o = ["a", "b", "c"];
console.log(o[1]);          // 打印 "b".

delete o[1];                // 刪除一個屬性.
console.log(o[1]);          // 打印 "undefined"; 第二個屬性不存在
o.__proto__ = {1: "B"};     // 在原型上定義第二個屬性

console.log(o[0]);          // 打印 "a".
console.log(o[1]);          // 打印 "B".
console.log(o[2]);          // 打印
console.log(o[3]);          // 打印 undefined複製代碼

簡言之,若是接收器上不存在屬性,咱們必須繼續在原型鏈上查找。若是元素是自包含的,咱們不在 HiddenClass 中存儲有關當前索引的屬性,咱們須要一個特殊的值,稱爲 the_hole,來標記該位置的屬性是不存在的。這個數組函數的性能是相當重要的。若是咱們知道有沒有缺省,即元素是連續的,咱們能夠不用昂貴代價來查詢原型鏈來進行本地操做。

快速元素和字典元素: 元素的第二個主要區別是它們是快速的仍是字典模式的。快速元素是簡單的 VM 內部數組,其中屬性索引映射到元素存儲區中的索引。然而,這種簡單的表示在稀疏數組中是至關浪費的。在這種狀況下,咱們使用基於字典的表示來節省內存,以訪問速度稍微慢一些爲代價:

const sparseArray = [];
sparseArray[1 << 20] = "foo"; // 使用字典元素建立一個數組。複製代碼

在這個例子中,若是分配一個 10K 的全排列會更浪費。因此取而代之的是 V8 建立的一個字典,咱們在其中存儲三個如出一轍的鍵值描述符。本例中的鍵爲 10000,值爲「字符串」還有一個默認描述符。由於咱們沒有辦法在 HiddenClass 存儲區描述細節,在 V8 中 當你定義一個索引屬性與自定義描述符存儲在慢元素中:

const array = [];
Object.defineProperty(array, 0, {value: "fixed", configurable});
console.log(array[0]);      // 打印 "fixed".
array[0] = "other value";   // 不能從新第 1 個索引.
console.log(array[0]);      // 仍然打印 "fixed".複製代碼

在這個例子中,咱們在數組上添加了一個 configurablefalse 的屬性。此信息存儲在慢元素字典三元組的描述符部分中。須要注意的是,在慢元素對象上,數組函數的執行速度要慢得多。

小整數和雙精度元素: 對於快速元素,V8中還有另外一個重要的區別。例如,若是你只保存整數數組,一個常見的例子:GC 沒有接受數組,由於整數直接編碼爲所謂的小整數(SMIS)。另外一個特例是數組,它們只包含雙精度數。不像SMIS,浮點數一般表示爲對象佔用的幾個字符。然而,V8 使用兩行來存儲純雙精度組,以免內存和性能開銷。下面的示例列出了 SMI 和雙精度元素的 4 個示例:

const a1 = [1,   2, 3];  // Smi Packed
const a2 = [1,    , 3];  // Smi Holey, a2[1] reads from the prototype
const b1 = [1.1, 2, 3];  // Double Packed
const b2 = [1.1,  , 3];  // Double Holey, b2[1] reads from the prototype複製代碼

特別的元素: 到目前爲止,咱們涵蓋了 20 種不一樣元素中的 7 種。爲簡單起見,咱們排除了 9 元種 數組類型,兩個字符串包裝等等,兩個參數對象。

ElementsAccessor: 你能夠想象咱們並不想爲了每一種元素在 C++ 中寫 20 次數組函數。這就是 C++ 的奇妙之處。爲了代替一次又一次數組函數的實現,咱們在從後備存儲訪問元素創建了 ElementsAccessor 。ElementsAccessor 依賴 CRTP 建立每個數組函數的專業版。因此,若是你調用數組中的一些方法例如 slice,將經過調用 V8 引擎的內部調用內置 C++ 編寫的,ElementsAccessor 的專業版:

本節重點:

  • 有快速模式和字典模式索引屬性和元素。
  • 快速屬性能夠被打包而且他們能夠包含被刪除索引屬性缺省的標誌。
  • 數組元素類型固定,以加速數組函數並減小 GC 開銷,方便引擎優化。

瞭解屬性如何工做是在 V8 中許多優化的關鍵。對於 JavaScript 開發人員來講,這些內部決策中有不少是不可見的,但它們解釋了爲何某些代碼模式比其餘代碼模式更快。更改屬性或元素類型一般讓 V8 創造不一樣的 HiddenClass,阻礙 V8 優化的緣由。敬請期待我之後的文章:V8 引擎 VM 內部是如何工做的。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索