V8中的隱藏類(Hidden Classes)和內聯緩存(Inline Caching)

隱藏類(Hidden Classes)

衆所周知,Javascript是一種動態編程語言,這意味着對象在初始化後仍然能夠對其屬性進行增刪操做。好比,下面這段代碼,「car」對象經初始化後帶有「make」和「model」兩個屬性,可是以後,該對象又被動態地添加了「year」這個屬性。javascript

function Car(make,model) {
    this.make = make;
    this.model = model;
}

const car = new Car(honda,accord);

car.year = 2005;

大多數的Javascript解釋器使用類字典結構來儲存類的屬性在內存中的位置。可是這樣的結構使得在進行屬性值的查找時,Javascript要比Java這種靜態語言更消耗性能。在Java中,全部對象的屬性在編譯前將會被一個固定的對象結構肯定下來,而且在運行時不能動態的進行增刪。這樣帶來的好處就是,屬性的值(或是屬性的指針)能夠彼此間間隔固定的偏移量儲存在一段連續的內存空間中。經過屬性的類型能夠輕鬆肯定它的偏移量,可是因爲Javascript中在運行時能夠動態地改變屬性類型,因此在Javascript中是這種方法是不可能實現的。java

在像Java這樣的非動態語言中,單個指令就能夠肯定屬性在內存中的位置,可是在Javascript中須要多個指令在哈希表中查找屬性的內存位置。這致使Javascript中的屬性查找相較於其餘語言要慢得多。git

鑑於字典表這種查找屬性內存位置的方式如此低效,V8使用了一種大相徑庭的方法進行改進,隱藏類。其實,拋開隱藏類做用在運行時的區別不談,它和Java中的固定對象結構十分類似。在閱讀下面的內容以前,請明確兩個重點,第一,V8會爲每一個對象關聯一個隱藏類,第二,隱藏類的目的是優化屬性的訪問速度。下面讓咱們進入正題。github

function Point(x,y) {
    this.x = x;
    this.y = y;
}

const obj = new Point(1,2);

一旦聲明瞭一個新的方法,Javascript就會建立一個隱藏類C0。編程

image

在此時尚未聲明任何的屬性,因此C0如今爲空。segmentfault

一旦第一個語句「this.x=x」被執行,V8將會基於C0建立第二個隱藏類C1。C1記錄了能夠找到屬性x在內存中的位置。在這個例子中,x保存在偏移量爲0的位置,這表示能夠將一個內存中的對象目標看做是一段連續的空間。而這段空間中的第一段偏移表明着屬性x。與此同時,V8將會用「類偏移」操做更新C0,這表明着屬性x已經添加到了目標對象。以後,目標對象所對應的隱藏類指針將指向C1。緩存

image

每當目標對象添加一個新的屬性,對象的舊的隱藏類就會變換路徑到一個新的隱藏類。隱藏類的重要之處在於可使通過相同建立過程建立的對象共享隱藏類。假如兩個對象共享一個隱藏類,並向兩個對象中同時添加相同的屬性,那麼這種變換將會保證變換後獲得相同的隱藏類,這樣代碼就獲得了優化。編程語言

當「this.y=y」執行時將重複上面的操做。一個新的叫C2的隱藏類將被建立,而後對C1進行類變換代表屬性y已經添加到了目標對象,最後將隱藏類指向C2。這樣目標對象的隱藏類就更新到了C2。性能

image

注意:隱藏類的變換取決於對目標對象的屬性添加順序。請注意下面的代碼:優化

1  function Point(x,y) {
2    this.x = x;
3    this.y = y;
4  }
5 
7  var obj1 = new Point(1,2);
8  var obj2 = new Point(3,4);
9
10 obj1.a = 5;
11 obj1.b = 10;
12
13 obj2.b = 10;
14 obj2.a = 5;

直到第九行爲止,obj1和obj2都共享同一個隱藏類。可是,當屬性a和b以相反的順序添加到了兩個對象中,這致使最後兩個對象以不一樣的變換路徑產生了兩個不一樣的的隱藏類。

image

看到這裏,有些讀者會認爲兩個對象具備兩個不一樣的隱藏類並非什麼嚴重的問題。只要隱藏類中儲存着正確的偏移量,訪問屬性的速度應該和共享相同隱藏類同樣快。想理解爲何這種想法是錯誤的,須要先介紹另外一種V8的優化技術,行內緩存。

行內緩存(Inline Caching)

V8利用的另外一種技術來優化動態類型語言的性能,叫作「行內緩存」。若是想詳細深刻地瞭解行內緩存,能夠參考這裏,可是簡單來說,行內緩存依賴於一種觀察到的現象,那就是,重複調用方法大機率會使用相同類型的參數。

那它究竟是如何工做的呢?V8將會維護一個記錄最近有一段時間內調用方法時傳入參數類型的緩存,而後使用所得到的信息預測在將來調用時所傳入的參數類型。一旦V8引擎對參數的類型進行了正確的預測,將使得引擎越過解析如何訪問類屬性的過程,直接使用以前緩存的信息直接得到隱藏類並對對象屬性進行訪問。

那麼爲何隱藏類的概念和行內緩存這兩個概念如此緊密相關呢?每當使用一個特定的對象調用方法時,V8引擎就會去查找對象的隱藏類,以便獲取後續訪問特定屬性的偏移量。通過兩次成功地以具備相同隱藏類參數調用相同的方法後,V8引擎將省略隱藏類的查找過程,並直接的添加屬性的偏移量。對於後續的調用,引擎都將假設隱藏類不會變化,並直接使用上一次查找時緩存的偏移訪問內存,這樣將極大地提高訪問速度。

行內緩存是對象共享隱藏類地一個重要緣由。若是你建立了兩個相同類型可是具備不一樣隱藏類的對象,引擎將沒法對其進行行內緩存的優化,由於不一樣隱藏類表明着具備不一樣的屬性偏移量。

image

優化相關建議

  1. 保證以相同的順序實例化對象屬性,這樣能夠保證它們共享相同的隱藏類。
  2. 在對象實例化後向對象添加屬性將會迫使隱藏類改變,這將會使也已經進行行內緩存的方法的訪問速度變慢。因此,請儘可能保證,在構造器內進行全部的屬性聲明。
  3. 不停執行相同方法的代碼會比總在執行不一樣方法的代碼速度快。
相關文章
相關標籤/搜索