本文爲譯文,原文地址:http://v8project.blogspot.com...,做者,@Camillo Bruni ,V8 JavaScript Engine Team Bloghtml
在這篇博客中,咱們想解釋 V8 如何在內部處理 JavaScript 屬性。從 JavaScript 的角度來看,屬性只有一些區別。JavaScript 對象主要表現爲字典,字符串做爲鍵名以及任意對象做爲鍵值。然而,該規範在迭代過程當中對整數索引(integer-indexed
)屬性和其它屬性進行了不一樣的處理。除此以外,不一樣的屬性的行爲大體相同,與它們是否爲整數索引無關。編程
然而,在 V8 引擎下,因爲性能和內存的緣由,確實依賴於幾種不一樣的屬性表示。在這篇博客中,咱們將介紹 V8 如何在處理動態添加的屬性時提供快速的屬性訪問。瞭解屬性的工做原理對於解釋諸如內聯緩存(inline caches
)在 V8 中的優化是相當重要的。數組
這篇博客解釋了處理整數索引和命名屬性(named properties
)的區別。以後咱們展現了當添加命名屬性時 V8 如何維護 HiddenClasses
,便於提供一種快速的方式來識別對象的形狀。而後,咱們將繼續深刻了解命名屬性如何針對快速訪問進行優化,或依據用途進行快速修改。在最後一個章節,咱們將提供有關 V8 如何處理整數索引或數組索引(array indices
)的詳細信息。緩存
咱們首先來分析一個簡單的對象,如 {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
)來節省內存。ide
命名屬性以相似的方式存儲在單獨的數組中。然而,不一樣於元素,咱們不能簡單地使用鍵推斷它所在數組中的位置,咱們須要一些額外的元數據。在 V8 中,每一個 JavaScript 對象都有一個 HiddenClass 關聯。HiddenClass 存儲有關對象形狀的信息,其中包括從屬性名到索引再到屬性的映射。爲了是事情複雜化,咱們有時會爲屬性而不是簡單的數組使用字典。咱們將在專門的章節中更詳細地解釋這一點。函數
從這一節開始:性能
在解釋元素和命名屬性的區別以後,咱們須要看看 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 建立一個單獨的分支。
從本節開始:
在概述 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
字典。
in-object
屬性直接存儲在對象自己上,並提供最快訪問。fast
屬性存儲在屬性中,全部元信息都存儲在 HiddenClass 的描述符數組中。slow
屬性存儲在自包含(self-contained)屬性字典中,元信息再也不經過 HiddenClass 共享slow
屬性容許有效的屬性刪除和添加,但訪問速度比其餘兩種類型更慢。到目前爲止,咱們已經查看了命名屬性,忽略了經常使用於數組的整數索引屬性。整數索引屬性的處理和命名屬性的複雜性相同。即便全部索引屬性始終在元素存儲中單獨存儲,也有 20 種不一樣類型的元素!
Packed 或 Holey 元素:V8 作出的第一個主要區別是元素是否支持存儲打包(packed)或有空位(holes)。若是你刪除索引元素,或者你沒有定義它,你將在後臺存儲中找到空位。一個簡單的例子是 [1,,3],第二個條目是一個空位。下面的例子說明了這個問題:
const o = "a", "b", "c" (); console.log(o1 ()); // Prints "b". delete o1 (); // Introduces a hole in the elements store. console.log(o1 ()); // Prints "undefined"; property 1 does not exist. o.proto = {1: "B"}; // Define property 1 on the prototype. console.log(o0 ()); // Prints "a". console.log(o1 ()); // Prints "B". console.log(o2 ()); // Prints "c". console.log(o3 ()); // Prints undefined
簡單來講,若是接收方不存在屬性,則必須繼續查找原型鏈。鑑於元素是獨立的,例如咱們不在 HiddenClass 上存儲有關當前索引屬性的信息,所以咱們須要一個名爲 the_hole 的特殊值來標記不存在的屬性。這對於數組很是重要。若是咱們知道沒有空位,即元素存儲被打包,咱們能夠執行本地操做,而沒必要浪費在原型鏈上查找。
Fast 或 Dictionary 元素:元素上第二個主要的區別是它們是 fast 仍是 dictionary 模式。fast 元素是簡單的 VM 內部數組,其中屬性索引映射到元素存儲中的索引。然而,對於只有少數條目被佔用的很是大的 sparse/holey 數組,這幾乎是至關浪費的。在這種狀況下,咱們使用基於字典的表示形式來節省內存,代價是訪問速度稍慢:
const sparseArray = (); sparseArray9999 () = "foo"; // Creates an array with dictionary elements.
在這個例子中,使用 10k 條目分配一個完整的數組那是至關浪費的。而 V8 會建立一個字典,咱們存儲一個鍵值描述符三元組。在這個例子中,鍵名會是 9999,鍵值爲 「foo」 和默認描述符。鑑於咱們沒有辦法在 HiddenClass 上存儲描述符詳細信息,因此當你使用自定義描述符定義索引屬性時,V8 將採用 slow 元素:
const array = (); Object.defineProperty(array, 0, {value: "fixed", configurable: false}); console.log(array0 ()); // Prints "fixed". array0 () = "other value"; // Cannot override index 0. console.log(array0 ()); // Still prints "fixed".
在這個例子中,咱們在數組中添加了一個不可配置的屬性。該信息存儲在 slow 元素字典三元組的描述符部分中。重要的是要注意,對於具備 slow 元素的對象,Array 函數執行的至關慢。
Smi 和 Double 元素:對於 fast 元素,V8 中還有另外一個重要區別。例如,若是隻將數組中的整數存儲在一個常見的用例中,則 GC 沒必要查看數組,由於整數直接編碼爲所謂的小整數(Smis)。另外一個特殊狀況是數組只包含 doubles。與 Smis 不一樣,浮點數一般表示爲佔據多個單詞的完整對象。然而,V8 存儲純雙數組的原始雙精度,以免內存和性能開銷。如下示例列出了 Smis 和 double 元素的 4 個示例:
const a1 = 1, 2, 3 (); // Smi Packed const a2 = 1, , 3 (); // Smi Holey, a21 () reads from the prototype const b1 = 1.1, 2, 3 (); // Double Packed const b2 = 1.1, , 3 (); // Double Holey, b21 () reads from the prototype
特殊元素:目前爲止,咱們涵蓋了 20 種不一樣元素中的 7 種。爲了簡單起見,咱們排除了 TpyedArrays 的 9 個元素類型,以及兩個用於字符串包裝,最後剩下兩個更特殊的元素種類的參數對象。
ElementsAccessor: 能夠想象,咱們並非徹底熱衷於在 C++ 中編寫數組函數 20 次,對於每個元素都是同樣。那就是展示 C++ 魔法的時刻了,而不是一遍遍地實現 Array 函數,咱們構建了 ElementsAccessor,只須要實現從後備存儲器訪問元素的簡單函數。ElementsAccessor 依賴於 CRTP 來建立每一個 Array 函數的專用版。所以,若是你在數組上調用 slice,V8 就會內部調用 C++ 編寫的內建函數,並經過 ElementsAccessor 調用該函數的專用版本:
從這一節開始:
瞭解屬性的工做原理是 V8 中許多優化的關鍵。對於 JavaScript 開發者,許多內部決策不是直接可見的,但它們解釋了爲何某些代碼模式比其餘代碼模式更快。更改屬性或元素類型一般會致使 V8 建立一個不一樣的 HiddenClass,這可能致使相似污染,從而阻止 V8 生成最佳代碼。請繼續關注 V8 的內部虛擬機的工做原理。