摘要:內存結構與屬性訪問詳解從屬于筆者的前端入門與工程實踐,推薦閱讀我的前端之路工具化與工程化。內存結構與屬性訪問上世紀九十年代,隨著網景瀏覽器的發(fā)行,首次進入人們的視線。
V8 Object 內存結構與屬性訪問V8 Object 內存結構與屬性訪問詳解從屬于筆者的Web 前端入門與工程實踐,推薦閱讀2016-我的前端之路:工具化與工程化。更多關于 JavaScript 引擎文章參考這里。
上世紀九十年代,隨著網景瀏覽器的發(fā)行,JavaScript 首次進入人們的視線。之后隨著 AJAX 的大規(guī)模應用與富客戶端、單頁應用時代的到來,JavaScript 在 Web 開發(fā)中占據(jù)了越來越重要的地位。在早期的 JavaScript 引擎中,性能越發(fā)成為了開發(fā)網頁應用的瓶頸。而 V8 引擎設計的目標就是為了保證大型 JavaScript 應用的執(zhí)行效率,在很多測試中可以明顯發(fā)現(xiàn) V8 的性能優(yōu)于 JScript (Internet Explorer), SpiderMonkey (Firefox), 以及 JavaScriptCore(Safari). 根據(jù) V8 的官方文檔介紹,其主要是從屬性訪問、動態(tài)機器碼生成以及高效的垃圾回收這三個方面著手性能優(yōu)化。Obejct 當屬 JavaScript 最重要的數(shù)據(jù)類型之一,本文我們對其內部結構進行詳細闡述。其繼承關系圖如下所示:
在 V8 中新分配的 JavaScript 對象結構如下所示:
[ class / map ] -> ... ; 指向內部類 [ properties ] -> [empty array] [ elements ] -> [empty array] ; 數(shù)值類型名稱的屬性 [ reserved #1 ] - [ reserved #2 ] | [ reserved #3 ] }- in object properties,即預分配的內存空間 ............... | [ reserved #N ] -/
在創(chuàng)建新的對象時,V8 會創(chuàng)建某個預分配的內存區(qū)域來存放所謂的 in-object 屬性,預分配區(qū)域的大小由構造函數(shù)中的參數(shù)數(shù)目決定(this.field = expr)。當你打算向對象中添加某個新屬性時,V8 首先會嘗試放入所謂的 in-order 槽位中,當 in-object 槽位過載之后,V8 會嘗試將新的屬性添加到 out-of-object 屬性列表。而屬性名與屬性下標的映射關系即存放在所謂隱藏類中,譬如{ a: 1, b: 2, c: 3, d: 4}對象的存儲方式可能如下:
[ class ] -> [a: in obj #1, b: in obj #2, c: out obj #1, d: out obj #2] [ properties ] -> [ 3 ][ 4 ] ; this is linear array [ elements ] [ 1 ] [ 2 ]
隨著屬性數(shù)目的增加,V8 會轉回到傳統(tǒng)的字典模式/哈希表模式:
[ class ] -> [ OBJECT IS IN DICTIONARY MODE ] [ properties ] -> [a: 1, b: 2, c: 3, d: 4, e: 5] ; this is classical hash table [ elements ]Reference
V8 Design Elements
A tour of V8: object representation
Demystifying v8 and JavaScript Performance
V8 Docs:Object Class Reference
How does V8 manage the memory of object instances?
Property Name:屬性名作為動態(tài)語言,JavaScript 允許我們以非常靈活的方式來定義對象,譬如:
obj.prop obj["prop"]
參照 JavaScript 定義規(guī)范中的描述,屬性名恒為字符串,即使你使用了某個非字符串的名字,也會隱式地轉化為字符串類型。譬如你創(chuàng)建的是個數(shù)組,以數(shù)值下標進行訪問,然而 V8 還是將其轉化為了字符串再進行索引,因此以下的方式就會獲得相同的效果:
obj[1]; // obj["1"]; // names for the same property obj[1.0]; // var o = { toString: function () { return "-1.5"; } }; obj[-1.5]; // also equivalent obj[o]; // since o is converted to string
而 JavaScript 中的 Array 只是包含了額外的length屬性的對象而已,length會返回當前最大下標加一的結果(此時字符串下標會被轉化為數(shù)值類型計算):
var a = new Array(); a[100] = "foo"; a.length; //101 a[undefined] = "a"; a.length; //0
Function本質上也是對象,只不過length屬性會返回參數(shù)的長度而已:
> a = ()=>{} [Function: a] > a.length 0 > a = (b)=>{} [Function: a] > a.length 1In-Object Properties & Fast Property Access:對象內屬性與訪問優(yōu)化
作為動態(tài)類型語言,JavaScript 中的對象屬性可以在運行時動態(tài)地增刪,意味著整個對象的結構會頻繁地改變。大部分 JavaScript 引擎傾向于使用字典類型的數(shù)據(jù)結構來存放對象屬性( Object Properties),每次進行屬性訪問的時候引擎都需要在內層中先動態(tài)定位屬性對應的下標地址然后讀取值。這種方式實現(xiàn)上比較容易,但是會導致較差的性能表現(xiàn)。其他的類似于 Java 與 Smalltalk 這樣的靜態(tài)語言中,成員變量在編譯階段即確定了其在內存中的固定偏移地址,進行屬性訪問的時候只需要單指令從內存中加載即可。而 V8 則利用動態(tài)創(chuàng)建隱藏內部類的方式動態(tài)地將屬性的內存地址記錄在對象內,從而提升整體的屬性訪問速度。總結而言,每當為某個對象添加新的屬性時,V8 會自動修正其隱藏內部類。我們先通過某個實驗來感受下隱藏類的存在:
var PROPERTIES = 10000000; var o = {}; var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start); function O(size) { for (var i = 0; i < size; i++) { this[i] = null; } } var o = new O(PROPERTIES); var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start); class OClass { constructor(size){ for (var i = 0; i < size; i++) { this[i] = null; } } } var o = new OClass(PROPERTIES); var start = +new Date; for (var i = 0; i < PROPERTIES; i++) { o[i] = i; } console.log(+new Date - start);
該程序的執(zhí)行結果如下:
// Babel 下結果 385 37 49 // Chrome 下結果 416 32 31
第一種實現(xiàn)中,每次為對象o設置新的屬性時,V8 都會創(chuàng)建新的隱藏內部類(內部稱為 Map)來存儲新的內存地址以優(yōu)化屬性查找速度。而第二種實現(xiàn)時,我們在創(chuàng)建新的對象時即初始化了內部類,這樣在賦值屬性時 V8 以及能夠高性能地定位這些屬性。第三種實現(xiàn)則是用的 ES6 Class,在純正的 V8 下性能最好。接下來我們具體闡述下隱藏類的工作原理,假設我們定義了描述點的函數(shù):
function Point(x, y) { this.x = x; this.y = y; }
當我們執(zhí)行new Point(x,y)語句時,V8 會創(chuàng)建某個新的Point對象。創(chuàng)建的過程中,V8 首先會創(chuàng)建某個所謂C0的隱藏內部類,因為尚未為對象添加任何屬性,此時隱藏類還是空的:
接下來調用首個賦值語句this.x = x;為當前Point對象創(chuàng)建了新的屬性x,此時 V8 會基于C0創(chuàng)建另一個隱藏類C1來替換C0,然后在C1中存放對象屬性x的內存位置信息:
這里從C0到C1的變化稱為轉換(Transitions),當我們?yōu)橥粋€類型的對象添加新的屬性時,并不是每次都會創(chuàng)建新的隱藏類,而是多個對象會共用某個符合轉換條件的隱藏類。接下來繼續(xù)執(zhí)行this.y = y 這一條語句,會為Point對象創(chuàng)建新的屬性。此時 V8 會進行以下步驟:
基于C1創(chuàng)建另一個隱藏類C1,并且將關于屬性y的位置信息寫入到C2中。
更新C1為其添加轉換信息,即當為Point對象添加屬性 y 時,應該轉換到隱藏類 C2。
整個過程的偽代碼描述如下:
Reused Hidden Class:重復使用的隱藏類Class C0 "x": TRANSITION to C1 at offset 0 this.x = x; Class C1 "x": FIELD at offset 0 "y": TRANSITION to C2 at offset 1 this.y = y; Map C2 "x": FIELD at offset 0 "y": FIELD at offset 1
我們在上文中提及,如果每次添加新的屬性時都創(chuàng)建新的隱藏類無疑是極大的性能浪費,實際上當我們再次創(chuàng)建新的Point對象時,V8 并不會創(chuàng)建新的隱藏類而是使用已有的,過程描述如下:
初始化新的Point對象,并將隱藏類指向C0。
添加x屬性時,遵循隱藏類的轉換原則指向到C1 , 并且根據(jù)C1指定的偏移地址寫入x。
添加y屬性時,遵循隱藏類的轉換原則指向到C2,并且根據(jù)C2指定的偏移地址寫入y。
另外我們在上文以鏈表的方式描述轉換,實際上真實場景中 V8 會以樹的結構來描述轉換及其之間的關系,這樣就能夠用于類似于下面的屬性一致而賦值順序顛倒的場景:
function Point(x, y, reverse) { if (reverse) { this.x = x; this.y = y; } else { this.y = x; this.x = y; } }Methods & Prototypes:方法與原型
JavaScript 中并沒有類的概念(語法糖除外),因此對于方法的調用處理會難于 C++ 或者 Java。下面這個例子中,distance方法可以被看做Point的普通屬性之一,不過其并非原始類型的數(shù)據(jù),而是指向了另一個函數(shù):
function Point(x, y) { this.x = x; this.y = y; this.distance = PointDistance; } function PointDistance(p) { var dx = this.x - p.x; var dy = this.y - p.y; return Math.sqrt(dx*dx + dy*dy); }
如果我們像上文介紹的普通的 in-object 域一樣來處理distance屬性,那么無疑會帶來較大的內存浪費,畢竟每個對象都要存放一段外部函數(shù)引用(Reference 的內存占用往往大于原始類型)。C++ 中則是以指向多個虛函數(shù)的虛函數(shù)表(V-Tables)解決這個問題。每個包含虛函數(shù)的類的實例都會指向這個虛函數(shù)表,當調用某個虛函數(shù)時,程序會自動從虛函數(shù)表中加載該函數(shù)的地址信息然后轉向到該地址調用。V8 中我們已經使用了隱藏類這一共享數(shù)據(jù)結構,因此可以很方便地改造下就可以。我們引入了所謂 Constant Functions 的概念,某個 Constant Function 即代表了對象中僅包含某個名字,而具體的屬性值存放在描述符本身的概念:
Class C0 "x": TRANSITION to C1 at offset 0 this.x = x; Class C1 "x": FIELD at offset 0 "y": TRANSITION to C2 at offset 1 this.y = y; Class C2 "x": FIELD at offset 0 "y": FIELD at offset 1 "distance": TRANSITION to C3 this.distance = PointDistance; Class C3 "x": FIELD at offset 0 "y": FIELD at offset 1 "distance": CONSTANT_FUNCTION
注意,在這里如果我們將PointDistance 重定義指向了其他函數(shù),那么這個轉換也會自動失效,V8 會創(chuàng)建新的隱藏類。另一種解決這個問題的方法就是使用原型,每個構造函數(shù)都會有所謂的Prototype屬性,該屬性會自動成為對象的原型鏈上的一環(huán),上面的例子可以改寫為以下方式:
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.distance = function(p) { var dx = this.x - p.x; var dy = this.y - p.y; return Math.sqrt(dx*dx + dy*dy); } ... var u = new Point(1, 2); var v = new Point(3, 4); var d = u.distance(v);
V8 同樣會把原型鏈上的方法在隱藏類中映射為 Constant Function 描述符,而調用原型方法往往會比調用自身方法慢一點,畢竟引擎不僅要去掃描自身的隱藏類,還要去掃描原型鏈上對象的隱藏類才能得知真正的函數(shù)調用地址。不過這個不會對于代碼的性能造成明顯的影響,因此寫代碼的時候也不必小心翼翼的避免這個。
Dictionary Mode對于復雜屬性的對象,V8 會使用所謂的字典模式(Dictionary Mode)來存儲對象,也就是使用哈希表來存放鍵值信息,這種方式存儲開銷會小于上文提到的包含了隱藏類的方式,不過查詢速度會遠小于前者。初始狀態(tài)下,哈希表中的所有的鍵與值都被設置為了undefined,當插入新的數(shù)據(jù)時,計算得出的鍵名的哈希值的低位會被當做初始的存儲索引地址。如果此地址已經被占用了,V8 會嘗試向下一個地址進行插入,直到插入成功,偽代碼表述如下:
// 插入 insert(table, key, value): table = ensureCapacity(table, length(table) + 1) code = hash(key) n = capacity(table) index = code (mod n) while getKey(table, index) is not undefined: index += 1 (mod n) set(table, index, key, value) //查找 lookup(table, key): code = hash(key) n = capacity(table) index = code (mod n) k = getKey(table, index) while k is not null or undefined and k != key: index += 1 (mod n) k = getKey(table, index) if k == key: return getValue(table, index) else: return undefined
盡管計算鍵名哈希值與比較的速度會比較快,但是每次讀寫屬性的時候都進行這么多步驟無疑會大大拉低速度,因此 V8 盡可能地會避免使用這種存儲方式。
Fast Elements:數(shù)值下標的屬性V8 中將屬性名為非負整數(shù)(0、1、2……)的屬性稱為Element,每個對象都有一個指向Element數(shù)組的指針,其存放和其他屬性是分開的。注意,隱藏類中并不包含 Element 的描述符,但可能包含其它有著不同 Element 類型的同一種隱藏類的轉換描述符。大多數(shù)情況下,對象都會有 Fast Element,也就是說這些 Element 以連續(xù)數(shù)組的形式存放。有三種不同的 Fast Element:
Fast small integers
Fast doubles
Fast values
根據(jù)標準,JavaScript 中的所有數(shù)字都理應以64位浮點數(shù)形式出現(xiàn)。因此 V8 盡可能以31位帶符號整數(shù)來表達數(shù)字(最低位總是0,這有助于垃圾回收器區(qū)分數(shù)字和指針)。因此含有Fast small integers類型的對象,其 Element 類型只會包含這樣的數(shù)字。如果需要存儲小數(shù)、大整數(shù)或其他特殊值,如-0,則需要將數(shù)組提升為 Fast doubles。于是這引入了潛在的昂貴的復制-轉換操作,但通常不會頻繁發(fā)生。Fast doubles 仍然是很快的,因為所有的數(shù)字都是無封箱存儲的。但如果我們要存儲的是其他類型,比如字符串或者對象,則必須將其提升為普通的 Fast Element 數(shù)組。
JavaScript 不提供任何確定存儲元素多少的辦法。你可能會說像這樣的辦法,new Array(100),但實際上這僅僅針對Array構造函數(shù)有用。如果你將值存在一個不存在的下標上,V8會重新開辟更大的內存,將原有元素復制到新內存。V8 可以處理帶空洞的數(shù)組,也就是只有某些下標是存有元素,而期間的下標都是空的。其內部會安插特殊的哨兵值,因此試圖訪問未賦值的下標,會得到undefined。當然,F(xiàn)ast Element 也有其限制。如果你在遠遠超過當前數(shù)組大小的下標賦值,V8 會將數(shù)組轉換為字典模式,將值以哈希表的形式存儲。這對于稀疏數(shù)組來說很有用,但性能上肯定打了折扣,無論是從轉換這一過程來說,還是從之后的訪問來說。如果你需要復制整個數(shù)組,不要逆向復制(索引從高到低),因為這幾乎必然觸發(fā)字典模式。
// 這會大大降低大數(shù)組的性能 function copy(a) { var b = new Array(); for (var i = a.length - 1; i >= 0; i--) b[i] = a[i]; return b; }
由于普通的屬性和數(shù)字式屬性分開存放,即使數(shù)組退化為字典模式,也不會影響到其他屬性的訪問速度(反之亦然)。
Object 代碼聲明// https://v8docs.nodesource.com/node-7.2/d4/da0/v8_8h_source.html#l02660 class V8_EXPORT Object : public Value { public: V8_DEPRECATE_SOON("Use maybe version", bool Set(Localkey, Local value)); V8_WARN_UNUSED_RESULT Maybe Set(Local context, Local key, Local value); V8_DEPRECATE_SOON("Use maybe version", bool Set(uint32_t index, Local value)); V8_WARN_UNUSED_RESULT Maybe Set(Local context, uint32_t index, Local value); // Implements CreateDataProperty (ECMA-262, 7.3.4). // // Defines a configurable, writable, enumerable property with the given value // on the object unless the property already exists and is not configurable // or the object is not extensible. // // Returns true on success. V8_WARN_UNUSED_RESULT Maybe CreateDataProperty(Local context, Local key, Local value); V8_WARN_UNUSED_RESULT Maybe CreateDataProperty(Local context, uint32_t index, Local value); // Implements DefineOwnProperty. // // In general, CreateDataProperty will be faster, however, does not allow // for specifying attributes. // // Returns true on success. V8_WARN_UNUSED_RESULT Maybe DefineOwnProperty( Local context, Local key, Local value, PropertyAttribute attributes = None); // Sets an own property on this object bypassing interceptors and // overriding accessors or read-only properties. // // Note that if the object has an interceptor the property will be set // locally, but since the interceptor takes precedence the local property // will only be returned if the interceptor doesn"t return a value. // // Note also that this only works for named properties. V8_DEPRECATED("Use CreateDataProperty / DefineOwnProperty", bool ForceSet(Local key, Local value, PropertyAttribute attribs = None)); V8_DEPRECATE_SOON("Use CreateDataProperty / DefineOwnProperty", Maybe ForceSet(Local context, Local key, Local value, PropertyAttribute attribs = None)); V8_DEPRECATE_SOON("Use maybe version", Local Get(Local key)); V8_WARN_UNUSED_RESULT MaybeLocal Get(Local context, Local key); V8_DEPRECATE_SOON("Use maybe version", Local Get(uint32_t index)); V8_WARN_UNUSED_RESULT MaybeLocal Get(Local context, uint32_t index); V8_DEPRECATED("Use maybe version", PropertyAttribute GetPropertyAttributes(Local key)); V8_WARN_UNUSED_RESULT Maybe GetPropertyAttributes( Local context, Local key); V8_DEPRECATED("Use maybe version", Local GetOwnPropertyDescriptor(Local key)); V8_WARN_UNUSED_RESULT MaybeLocal GetOwnPropertyDescriptor( Local context, Local key); V8_DEPRECATE_SOON("Use maybe version", bool Has(Local key)); V8_WARN_UNUSED_RESULT Maybe Has(Local context, Local key); V8_DEPRECATE_SOON("Use maybe version", bool Delete(Local key)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe Delete(Local context, Local key); V8_DEPRECATED("Use maybe version", bool Has(uint32_t index)); V8_WARN_UNUSED_RESULT Maybe Has(Local context, uint32_t index); V8_DEPRECATED("Use maybe version", bool Delete(uint32_t index)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe Delete(Local context, uint32_t index); V8_DEPRECATED("Use maybe version", bool SetAccessor(Local name, AccessorGetterCallback getter, AccessorSetterCallback setter = 0, Local data = Local (), AccessControl settings = DEFAULT, PropertyAttribute attribute = None)); V8_DEPRECATED("Use maybe version", bool SetAccessor(Local name, AccessorNameGetterCallback getter, AccessorNameSetterCallback setter = 0, Local data = Local (), AccessControl settings = DEFAULT, PropertyAttribute attribute = None)); // TODO(dcarney): mark V8_WARN_UNUSED_RESULT Maybe SetAccessor(Local context, Local name, AccessorNameGetterCallback getter, AccessorNameSetterCallback setter = 0, MaybeLocal data = MaybeLocal (), AccessControl settings = DEFAULT, PropertyAttribute attribute = None); void SetAccessorProperty(Local name, Local getter, Local setter = Local (), PropertyAttribute attribute = None, AccessControl settings = DEFAULT); Maybe HasPrivate(Local context, Local key); Maybe SetPrivate(Local context, Local key, Local value); Maybe DeletePrivate(Local context, Local key); MaybeLocal GetPrivate(Local context, Local key); V8_DEPRECATE_SOON("Use maybe version", Local GetPropertyNames()); V8_WARN_UNUSED_RESULT MaybeLocal GetPropertyNames( Local context); V8_WARN_UNUSED_RESULT MaybeLocal GetPropertyNames( Local context, KeyCollectionMode mode, PropertyFilter property_filter, IndexFilter index_filter); V8_DEPRECATE_SOON("Use maybe version", Local GetOwnPropertyNames()); V8_WARN_UNUSED_RESULT MaybeLocal GetOwnPropertyNames( Local context); V8_WARN_UNUSED_RESULT MaybeLocal GetOwnPropertyNames( Local context, PropertyFilter filter); Local GetPrototype(); V8_DEPRECATED("Use maybe version", bool SetPrototype(Local prototype)); V8_WARN_UNUSED_RESULT Maybe SetPrototype(Local context, Local prototype); Local
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81320.html
摘要:這些是中可用的最快屬性。通常來說我們將線性屬性存儲中存儲的屬性稱為。因此也支持所謂的屬性。整數(shù)索引屬性的處理和命名屬性的復雜性相同。 本文為譯文,原文地址:http://v8project.blogspot.com...,作者,@Camillo Bruni ,V8 JavaScript Engine Team Blog 在這篇博客中,我們想解釋 V8 如何在內部處理 JavaScrip...
摘要:在運行腳本時,需要顯示的指定對象。大對象區(qū)每一個區(qū)域都是由一組內存頁構成的。這里是唯一擁有執(zhí)行權限的內存區(qū)。換句話說,是該對象被之后所能回收到內存的總和。一旦活躍對象已被移出,則在舊的半空間中剩下的任何死亡對象被丟棄。 內存管理 本文以V8為背景 對之前的文章進行重新編輯,內容做了很多的調整,使其具有邏輯更加緊湊,內容更加全面。 1. 基礎概念 1.1 生命周期 不管什么程序語言,內存...
摘要:的內存限制和垃圾回收機制內存限制內存限制一般的后端語言開發(fā)中,在基本的內存使用是沒有限制的。的內存分代目前沒有一種垃圾自動回收算法適用于所有場景,所以的內部采用的其實是兩種垃圾回收算法。 前言 從前端思維轉變到后端, 有一個很重要的點就是內存管理。以前寫前端因為只是在瀏覽器上運行, 所以對于內存管理一般不怎么需要上心, 但是在服務器端, 則需要斤斤計較內存。 V8的內存限制和垃圾回收機...
摘要:關注于運行中的內存信息的展示,用可視化的方式還原了,有助于理解內存管理。背景運行過程中的大部分數(shù)據(jù)都保存在堆中,所以性能分析另一個比較重要的方面是內存,也就是堆的分析。上周發(fā)布了工具,可以用來動態(tài)地展示的結果,分析各種函數(shù)的調用關系。 OneHeap 關注于運行中的 JavaScript 內存信息的展示,用可視化的方式還原了 HeapGraph,有助于理解 v8 內存管理。 ...
閱讀 1078·2021-11-25 09:43
閱讀 699·2021-11-22 14:45
閱讀 3825·2021-09-30 09:48
閱讀 1065·2021-08-31 09:41
閱讀 1976·2019-08-30 13:52
閱讀 1981·2019-08-30 11:24
閱讀 1350·2019-08-30 11:07
閱讀 956·2019-08-29 12:15