標籤:

V8 中的 Fast 屬性

> 本文為譯文,原文地址:[v8project.blogspot.com/] ,作者,@Camillo Bruni ,V8 JavaScript Engine Team Blog

在這篇博客中,我們想解釋 V8 如何在內部處理 JavaScript 屬性。從 JavaScript 的角度來看,屬性只有一些區別。JavaScript 對象主要表現為字典,字元串作為鍵名以及任意對象作為鍵值。然而,該規範在迭代過程中對整數索引(integer-indexed)屬性和其它屬性進行了不同的處理。除此之外,不同的屬性的行為大致相同,與它們是否為整數索引無關。

然而,在 V8 引擎下,由於性能和內存的原因,確實依賴於幾種不同的屬性表示。在這篇博客中,我們將介紹 V8 如何在處理動態添加的屬性時提供快速的屬性訪問。了解屬性的工作原理對於解釋諸如內聯緩存(inline caches)在 V8 中的優化是至關重要的。

這篇博客解釋了處理整數索引和命名屬性(named properties)的區別。之後我們展示了當添加命名屬性時 V8 如何維護 HiddenClasses,便於提供一種快速的方式來識別對象的形狀。然後,我們將繼續深入了解命名屬性如何針對快速訪問進行優化,或依據用途進行快速修改。在最後一個章節,我們將提供有關 V8 如何處理整數索引或數組索引(array indices)的詳細信息。

命名屬性(Named Properties)和元素(Elements)

我們首先來分析一個簡單的對象,如 {a: "foo", b: "bar"}。該對象有兩個命名屬性,「a」 和 「b」。它是沒有任何屬性名稱的整數索引。數組索引(array-indexed properties)的屬性(通常稱為元素),在數組上最為突出。例如,數組 ["foo", "bar"],有兩個數組索引屬性:0,值為「foo」,1,值為「bar」。這是 V8 處理屬性的第一個主要區別。

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

元素和屬性存儲在兩個單獨的數據結構中,這使得添加和訪問屬性或元素對於不同的使用模式更有效。

元素主要用於各種 Array.prototype 方法,如 pop 或 slice。假設這些函數訪問連續範圍內的屬性,V8 大部分時間上也將它們內部表示為簡單的數組。在這篇文章的後面,我們將會解釋我們如何切換到基於稀疏字典的表示(sparse dictionary-based representation)來節省內存。

命名屬性以類似的方式存儲在單獨的數組中。然而,不同於元素,我們不能簡單地使用鍵推斷它所在數組中的位置,我們需要一些額外的元數據。在 V8 中,每個 JavaScript 對象都有一個 HiddenClass 關聯。HiddenClass 存儲有關對象形狀的信息,其中包括從屬性名到索引再到屬性的映射。為了是事情複雜化,我們有時會為屬性而不是簡單的數組使用字典。我們將在專門的章節中更詳細地解釋這一點。

從這一節開始:

  • 數組索引的屬性存儲在單獨的元素存儲中。
  • 命名的屬性存儲在屬性存儲中。
  • 元素和屬性可以是數組或字典。
  • 每個 JavaScript 對象都有一個關聯的 HiddenClass,它保存關於對象形狀的信息。

HiddenClass 和 DescriptorArrays

在解釋元素和命名屬性的區別之後,我們需要看看 HiddenClass 在 V8 中的工作原理。HiddenClass 存儲有關對象的元信息,包括對象上的屬性以及對象原型的引用數量。HiddenClass 在概念上類似與典型的面向對象編程語言中的類。然而,在基於原型的語言(如 JavaScript )中,通常不可能預先知道類。因此,在這種情況下,V8 引擎的 HiddenClass 是隨機創建的,並隨著對象的改變而動態更新。HiddenClass 作為對象形狀的標識符,並且是 V8 優化編譯器和內聯緩存(inline caches)的一個非常重要的組成部分。例如,優化編輯器可以直接內聯屬性訪問,如果它可以通過 HiddenClass 確保兼容對象結構。

讓我們來看看 HiddenClass 的重要部分。

在 V8 中,JavaScript 對象的第一個欄位指向一個 HiddenClass。(實際上,這是在 V8 堆上由垃圾收集器管理的任何對象的情況)。在屬性方面,最重要的信息是存儲屬性數量的第三位欄位和指向描述符數組的指針。描述符數組包含有關命名屬性的信息,如名稱本身和存儲值的位置。請注意,我們在這裡不跟蹤整數索引屬性,因此描述符數組中沒有條目。

關於 HiddenClass 的基本假設是具有相同結構的對象。例如,相同的命名屬性以相同的順序共享相同的 HiddenClass。為了實現這一點,當一個屬性被添加到一個對象時,我們使用一個不同的 HiddenClass。在下面的例子中,我們從一個空對象開始,並添加三個命名屬性。

每次添加新的屬性時,對象的 HiddenClass 都會被更改。在後台 V8 創建一個將 HiddenClass 鏈接在一起的轉換樹。V8 知道當你向空對象添加屬性「a」時要使用哪個 HiddenClass。如果以相同的順序添加相同的屬性,則此轉換樹將確保最終具有相同的最終 HiddenClass。以下實例顯示,即使我們在兩者之間添加簡單的索引屬性,也將遵循相同的轉換樹。

然而,如果我們創建一個新的對象來獲取不同的屬性,在這種情況下,屬性「b」,V8 將為新的 HiddenClass 創建一個單獨的分支。

從本節開始:

  • 具有相同結構(相同屬性的相同順序)的對象具有相同的 HiddenClass。
  • 默認情況下,添加的每個新的命名屬性都將創建一個新的 HiddenClass。
  • 添加數組索引屬性不會創建新的 HiddenClass。

三種不同類型的命名屬性

在概述 V8 如何使用 HiddenClass 跟蹤對象的形狀之後,讓我們來看就這些屬性實際是如何存儲的。如上面的介紹所述,有兩種基本類型的屬性:命名和索引。以下部分包含命名屬性。

一個簡單的對象,如 {a: 1, b: 2},可以在 V8 中有各種內部表現。雖然 JavaScript 的行為或多或少與外部的簡單字典相似,但 V8 視圖避免使用字典,因為它們阻礙了一些優化,例如內聯緩存,我們將在單獨的文章中解釋。

In-object 和 Normal Properties:V8 支持直接存儲在對象本身上的所謂 in-object 屬性。這些是 V8 中可用的最快屬性。因為它們可以無間接訪問。對象 in-object 的數量由對象的初始大小預先確定。如果對象中有空格添加了更多屬性,它們將被存儲在屬性存儲中。屬性存儲添加了一個間接級別,但可以獨立生長。

fast 和 slow 屬性:下一個重要區別在於 fast 和 slow 之間的屬性。通常來說我們將線性屬性存儲中存儲的屬性稱為「fast」。fast屬性是可以簡單的通過索引來訪問的。要從屬性的名稱到屬性存儲中的實際位置,我們必須先查看 HiddenClass 中的描述符數組,如前所述。

然而,如果許多屬性從對象中添加和刪除,則可能會生成大量時間和內存開銷來維護描述符數組和 HiddenClass。因此,V8 也支持所謂的 slow 屬性。具有 slow 屬性的對象具有自包含的字典作為屬性存儲。所有屬性元信息不再存儲在 HiddenClass 中的描述符數組中,而是直接存儲在屬性字典中。因此,可以添加和刪除屬性,而無需更新 HiddenClass。由於內聯緩存不能與字典屬性一起使用,後者通常比 fast 屬性慢。

從這一節開始:

  • 有三種不同的命名屬性類型:in-object,fast 和 slow 字典。
  1. in-object 屬性直接存儲在對象本身上,並提供最快訪問。
  2. fast 屬性存儲在屬性中,所有元信息都存儲在 HiddenClass 的描述符數組中。
  3. slow 屬性存儲在自包含(self-contained)屬性字典中,元信息不再通過 HiddenClass 共享
  • slow 屬性允許有效的屬性刪除和添加,但訪問速度比其他兩種類型更慢。

元素或數組索引屬性

到目前為止,我們已經查看了命名屬性,忽略了常用於數組的整數索引屬性。整數索引屬性的處理和命名屬性的複雜性相同。即使所有索引屬性始終在元素存儲中單獨存儲,也有 20 種不同類型的元素!

Packed 或 Holey 元素:V8 做出的第一個主要區別是元素是否支持存儲打包(packed)或有空位(holes)。如果你刪除索引元素,或者你沒有定義它,你將在後台存儲中找到空位。一個簡單的例子是 [1,,3],第二個條目是一個空位。下面的例子說明了這個問題:

const o = "a", "b", "c" ();nconsole.log(o1 ()); // Prints "b".nndelete o1 (); // Introduces a hole in the elements store.nconsole.log(o1 ()); // Prints "undefined"; property 1 does not exist.no.proto = {1: "B"}; // Define property 1 on the prototype.nnconsole.log(o0 ()); // Prints "a".nconsole.log(o1 ()); // Prints "B".nconsole.log(o2 ()); // Prints "c".nconsole.log(o3 ()); // Prints undefinedn

簡單來說,如果接收方不存在屬性,則必須繼續查找原型鏈。鑒於元素是獨立的,例如我們不在 HiddenClass 上存儲有關當前索引屬性的信息,因此我們需要一個名為 the_hole 的特殊值來標記不存在的屬性。這對於數組非常重要。如果我們知道沒有空位,即元素存儲被打包,我們可以執行本地操作,而不必浪費在原型鏈上查找。

Fast 或 Dictionary 元素:元素上第二個主要的區別是它們是 fast 還是 dictionary 模式。fast 元素是簡單的 VM 內部數組,其中屬性索引映射到元素存儲中的索引。然而,對於只有少數條目被佔用的非常大的 sparse/holey 數組,這幾乎是相當浪費的。在這種情況下,我們使用基於字典的表示形式來節省內存,代價是訪問速度稍慢:

const sparseArray = ();nsparseArray9999 () = "foo"; // Creates an array with dictionary elements.n

在這個例子中,使用 10k 條目分配一個完整的數組那是相當浪費的。而 V8 會創建一個字典,我們存儲一個鍵值描述符三元組。在這個例子中,鍵名會是 9999,鍵值為 「foo」 和默認描述符。鑒於我們沒有辦法在 HiddenClass 上存儲描述符詳細信息,所以當你使用自定義描述符定義索引屬性時,V8 將採用 slow 元素:

const array = ();nObject.defineProperty(array, 0, {value: "fixed", configurable: false});nconsole.log(array0 ()); // Prints "fixed".narray0 () = "other value"; // Cannot override index 0.nconsole.log(array0 ()); // Still prints "fixed".n

在這個例子中,我們在數組中添加了一個不可配置的屬性。該信息存儲在 slow 元素字典三元組的描述符部分中。重要的是要注意,對於具有 slow 元素的對象,Array 函數執行的相當慢。

Smi 和 Double 元素:對於 fast 元素,V8 中還有另一個重要區別。例如,如果只將數組中的整數存儲在一個常見的用例中,則 GC 不必查看數組,因為整數直接編碼為所謂的小整數(Smis)。另一個特殊情況是數組只包含 doubles。與 Smis 不同,浮點數通常表示為佔據多個單詞的完整對象。然而,V8 存儲純雙數組的原始雙精度,以避免內存和性能開銷。以下示例列出了 Smis 和 double 元素的 4 個示例:

const a1 = 1, 2, 3 (); // Smi Packednconst a2 = 1, , 3 (); // Smi Holey, a21 () reads from the prototypenconst b1 = 1.1, 2, 3 (); // Double Packednconst b2 = 1.1, , 3 (); // Double Holey, b21 () reads from the prototypen

特殊元素:目前為止,我們涵蓋了 20 種不同元素中的 7 種。為了簡單起見,我們排除了 TpyedArrays 的 9 個元素類型,以及兩個用於字元串包裝,最後剩下兩個更特殊的元素種類的參數對象。

ElementsAccessor: 可以想像,我們並不是完全熱衷於在 C++ 中編寫數組函數 20 次,對於每一個元素都是一樣。那就是展現 C++ 魔法的時刻了,而不是一遍遍地實現 Array 函數,我們構建了 ElementsAccessor,只需要實現從後備存儲器訪問元素的簡單函數。ElementsAccessor 依賴於 CRTP 來創建每個 Array 函數的專用版。因此,如果你在數組上調用 slice,V8 就會內部調用 C++ 編寫的內建函數,並通過 ElementsAccessor 調用該函數的專用版本:

從這一節開始:

  • fast 和字典模式索引的屬性和元素
  • fast 屬性可以打包,也可以包含指示索引屬性已被刪除的空位。
  • 元素專門針對其內容來加快陣列功能並降低 GC 開銷。

了解屬性的工作原理是 V8 中許多優化的關鍵。對於 JavaScript 開發者,許多內部決策不是直接可見的,但它們解釋了為什麼某些代碼模式比其他代碼模式更快。更改屬性或元素類型通常會導致 V8 創建一個不同的 HiddenClass,這可能導致類似污染,從而阻止 V8 生成最佳代碼。請繼續關注 V8 的內部虛擬機的工作原理。

推薦閱讀:

Chrome DevTools: 在 Profile 性能分析中顯示原生 JS 函數
chrome瀏覽器頁面渲染工作原理淺析
隱藏在 Node.js 浮點反序列化錯誤背後的故事
超大文件如何計算md5?

TAG:V8 |