new Fn(...) vs. Object.create(P)

這篇翻譯自 mraleph文章,順便當作是自己的memo

V8裡物件設計的基礎

在V8裡每個JavaScript物件長的都像這樣

js object

有些重要的事項要先紀錄在這裡:

  • 物件可以有0個物件內屬性空間(in-object property slots)並用字典來處理物件外屬性儲存空間(out-of-object property storage)。這是很普通且慢的一種JavaScript物件表現方式(又稱為字典模式)。快速模式的JavaScript則有0個或以上的物件內屬性空間及利用陣列來處理物件外屬性儲存空間。
  • 越多屬性存在物件內屬性空間越好:他將減少間接存取這些屬性,也不會浪費記憶體空間來處理物件外屬性儲存空間的陣列標頭。
  • 一旦物件被配置後就不可能再增加物件內屬性空間屬性的數量。若程式持續加屬性到一個已經沒有物件內屬性空間的物件,則新增的屬性將會被動態的加到物件外屬性儲存空間 ー 當然,動態增加也消耗很大。這就是為什麽有個好的關於估算一個物件將總共擁有多少屬性的方法非常重要。
  • 隱藏類別(Hidden Class)(又稱為 map)完全描繪了物件的樣板:該物件多大,有哪些屬性,有多少物件內屬性空間已經使用及已使用在哪(對於快速模式的物件)等等。隱藏類別基本上無法變更,每次有新屬性加到該物件時,該物件就會換到新的隱藏類別。

範例

讓我們想像一下「如果V8決定給一個物件字面值1個物件內屬性空間然後我們加了三個屬性到該物件」會發生什麽事。

1
2
3
4
var obj = {};
obj.x = 0;
obj.y = 1;
obj.z = 2;

堆疊中物件的演變將運作如下:

  1. 一開始他會是空的,並有一個”空”的隱藏類別

    first step

  2. 一旦我們加了x後隱藏類別就會換至新隱藏類別,也就是說該物件包含屬性存在第一個物件內屬性空間且他的值存在該空間裡。

    second step

  3. 當我們試著加y時,已經沒有空間在物件裡,所以V8將配置一個陣列給物件外屬性並儲存y的值在裡面。隱藏類別將再換一次,並映射至新隱藏類別。

    third step

    注意V8會多配置點物件外屬性儲存空間,因為他預期會有更多屬性(一次增加一個屬性到該儲存空間將會花平方複雜度且產生垃圾)

  4. 當我們試著加z時,物件裡面還是沒有空間,但在物件外儲存空間有足夠的空間,所以V8會使用他

    fourth step

    這個範例再次描繪出為何透過在物件配置前估算物件內屬性空間的數量來達成將所有屬性加至物件最有效的表現是非常重要的。

    註記: 如果我們再次執行同一段程式碼,將沒有新隱藏類別會被配置,最後我們會使用同一個隱藏類別。這是一個很重要的不變條件,V8中許多優化都以此為中心。

估算預期的屬性數量

當我們透過建構子new Fn(...)來配置第一個物件時,V8必須替建構子創造一個初始隱藏類別: 在運作建構子執行任何陳述式前,這將會是建構體最剛開始時物件的隱藏類別。當V8配置初始隱藏類別時,他必須決定要使用多少物件內屬性空間來保存屬性在物件裡,而且就像上面註記的,一旦配置後就沒辦法再給他更多物件內屬性空間(詳細後面會再說)。

V8在這裡做了什麽:他會走遍Fn建構子來查看this.x形式的指定(assignment),算有幾個指定並利用這個數字加上一些多餘空間來當作該物件屬性預期的數字。

樂觀的來看,不浪費記憶體來增加多餘空間是可能的,因為V8也做一些運行時效能分析(runtime profiling),稱作物件內多餘空間追蹤(in-object slack tracking) ー 運行時在一定數量的配置被配置後,將查看最大數量已使用的物件內屬性空間然後減少保留的空間數量至與多餘空間相符。因為小心挑選將值在運行時配置時放到未使用的物件內屬性空間,V8能夠藉由重寫儲存在各個隱藏類別的實體大小來縮小屬於同個轉換樹(transition tree)的所有已配置實體,且物件本身不須要被修正。

new Fn(…) vs. Object.create(P)

現在我們已經準備深入為什麽使用建構子的效能會比 Object.create 語法的效能好。

如果我們從Kyle的benchmark來調校new Fn(...)方法,我們將看到大多時間會花在已生成的code。然而從Object.create(P)的資料透露出V8運行時系統的兩個熱點負責整個執行時間的80% ー SharedStoreIC_ExtendStorage (50%) 及 Runtime_SetPrototype (30%)。

SharedStoreIC_ExtendStorage

如上面所解釋,V8負責估算預留物件內屬性空間數量的啟發式演算法以建構子為中心且不包括基本上實作起來像這樣的Object.create(P):

1
2
3
Object.create = function (P) {
return { __proto__: P };
};

導致誤見得到相同數量的物件內屬性空間,任何其他被配置的物件透過空的物件字面值將得到0 ー 在此範例中明顯不足,他將迫使V8增加物件外屬性儲存空間來讓新屬性被增加。

Runtime_SetPrototype

現在在V8設定一個物件的原型(prototype)需要進入運行時系統(runtime system)並在運行時系統中為一個物件找到一個新的適當的隱藏類別然後將一個物件轉換成他。

Q&A

V8能夠做什麽事來改善Object.create(P)的效能?

一定是可以的。有兩件事能夠被增進:

  1. 一般屬性儲存不需要在運行時(runtime) (即使他們將導致換新隱藏類別)。在相似的方式下,能夠透過使用__proto__儲存避免總是進入運行時來改善效能。
  2. Object.create應該以類似建構子的方式對待。我們只必須了解有固定P的 { __proto__: P } 的隱藏類別基本上就像建構子的初始map。意思是我們可以先多配置物件內屬性空間,然後依賴多餘空間追蹤來替我們縮小物件(雖然他當然需要將多餘空間追蹤概念化)。

為什麽在些許配滯後不換至”較大的”隱藏類別

多型(Polymorphism)是V8選擇縮小物件而不是當他發現一開始時給不足物件內屬性空間時便開始配置較大的物件的主因。

如上面所敘述的,我們沒辦法在物件被配置後將他變大,也代表VM沒辦法僅僅重寫轉換樹(transition tree)及增加一些數量至儲存在每個隱藏類別的實體大小。

反而,唯一的可能是重複轉換樹(transition tree),增加儲存在裡面的大小,然後轉換建構子的初始map指向新的那個。

然而,在這個地方,建構子將開始產生帶有新隱藏類別的物件,這也將多型轉換所有的位置看到舊有的已建構物件及新的已建構物件。

這裡一定有辦法避免此多型,但沒有一個方法是簡單或是無缺點的。例如,你可能會介紹一個主張是沒被紀錄在IC(inline caching及在GC(當GC是唯一的運行時能夠轉換及增長物件時)時被尚未棄用的部份所取代的棄用的隱藏類別。