摘要:這些是中可用的最快屬性。通常來說我們將線性屬性存儲中存儲的屬性稱為。因此也支持所謂的屬性。整數索引屬性的處理和命名屬性的復雜性相同。
本文為譯文,原文地址:http://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 字典。
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 調用該函數的專用版本:
從這一節開始:
fast 和字典模式索引的屬性和元素
fast 屬性可以打包,也可以包含指示索引屬性已被刪除的空位。
元素專門針對其內容來加快陣列功能并降低 GC 開銷。
了解屬性的工作原理是 V8 中許多優化的關鍵。對于 JavaScript 開發者,許多內部決策不是直接可見的,但它們解釋了為什么某些代碼模式比其他代碼模式更快。更改屬性或元素類型通常會導致 V8 創建一個不同的 HiddenClass,這可能導致類似污染,從而阻止 V8 生成最佳代碼。請繼續關注 V8 的內部虛擬機的工作原理。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91790.html
摘要:內存結構與屬性訪問詳解從屬于筆者的前端入門與工程實踐,推薦閱讀我的前端之路工具化與工程化。內存結構與屬性訪問上世紀九十年代,隨著網景瀏覽器的發行,首次進入人們的視線。 V8 Object 內存結構與屬性訪問詳解從屬于筆者的Web 前端入門與工程實踐,推薦閱讀2016-我的前端之路:工具化與工程化。更多關于 JavaScript 引擎文章參考這里。 V8 Object 內存結構與屬性訪問...
摘要:可以更有效地處理密集數組。然后有人提出了一個疑問為什么先指定長度再初始化測試出來會快一點其實,兩者相比只是可能變慢。具體因素有很多,比如預分配一個很大的數組,這時可以變快,的函數就是這么做的。如果數組很大,預先分配大小后性能反而會提升。 在我的上一篇文章 JavaScript 在 V8 中的元素種類及性能優化 中寫道: showImg(https://segmentfault.com...
摘要:內容爲該問題下的答案是對的學習筆記。一個叫做的過程用來決定對象的大小,其後的屬性作爲使用單獨的數組儲存。其中,包括然而有時也會降級爲當然由於分開儲存,降級並不會影響到其它類型的屬性。 內容爲該問題下的答案:http://segmentfault.com/q/1010000002423380 是對 http://jayconrod.com/posts/52/a-tour-of-v8-o...
摘要:對于每個前端程序員來講都有一個終極理想,那就是搞懂引擎是如何工作的。性能經過了兩次飛躍第次飛躍是年發布,第次則是年的。從去年底開始連載源碼分析,記錄一下自己學習源碼的點點滴滴。月星期六晚點和大家一起聊聊引擎前端程序員應該懂點知識講堂。 對于每個前端程序員來講都有一個終極理想,那就是搞懂 javascript 引擎是如何工作的。 從我的網絡 ID(justjavac)可以看出來,當我開始...
摘要:所做的最重要的事情,就是對成千上萬的網頁進行排序,所以它存在的意義是基于網頁的。確實有很多非常成功的產品,比如,,,但是它們其實都是收購來的。為什么呢因為要做到極簡主義,需要深刻思考用戶需求以及產品價值。 摘要: Chrome改變世界。 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數中的this究竟是什么鬼? JavaScript深入淺出第2課:...
閱讀 2521·2023-04-26 02:57
閱讀 1403·2023-04-25 21:40
閱讀 2155·2021-11-24 09:39
閱讀 3557·2021-08-30 09:49
閱讀 760·2019-08-30 15:54
閱讀 1166·2019-08-30 15:52
閱讀 2068·2019-08-30 15:44
閱讀 1274·2019-08-28 18:27