共計 3680 個字符,預(yù)計需要花費 10 分鐘才能閱讀完成。
如何分析 JS 引擎中的 Inline Cache 技術(shù),很多新手對此不是很清楚,為了幫助大家解決這個難題,下面丸趣 TV 小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
01 引例
function Point(x,y) {
this.x = x;
this.y = y;
}
var p = new Point(0, 1);
var q = new Point(2,3);
var r = new Point(4,5);
為了避免 API 調(diào)用不穩(wěn)定因素的影響,通過修改 V8 源碼,在內(nèi)部插入時間戳的方式。在 3.2G 8 核機器上,分別測試三次調(diào)用 new Point(x,y) 時執(zhí)行 this.x= x 這個語句耗時,結(jié)果如下表所示。
執(zhí)行代碼 this.x= x 耗時統(tǒng)計
var p = new Point(0,1);4.11nsvar q = new Point(2,3);6.63nsvar r = new Point(4,5);0.65ns
從表中的結(jié)果可以看出,事實并非想象中的從第二次執(zhí)行開始,速度就會變快,相反,第二次比第一次還要慢,到第三次的時候速度才會變快。本文后面會通過分析 V8 IC 機制來解釋為什么第二次速度最慢,第三次執(zhí)行速度又變快的原因。
02
問題分析
1. 對象的隱藏類 (Hidden Class)
由于 JavaScript 對象沒有類型信息,幾乎所有 JS 引擎都采用隱藏類(Hidden Class/Shape/Map 等)來描述對象的布局信息,用以在虛擬機內(nèi)部區(qū)分不同對象的類型,從而完成一些基于類型的優(yōu)化。
V8 對 JavaScript 對象都使用 HeapObject 來描述和存儲,每一種 JavaScript 對象都是 HeapObject 的子類,而每個 HeapObject 都用 Map 來描述對象的布局。對象的 Map 描述了對象的類型,即成員數(shù)目、成員名稱、成員在內(nèi)存中的位置信息等。
上述源碼中對象 p、q、r 都是由同一個構(gòu)造函數(shù) Point 生成,因此他們具有同樣的內(nèi)存布局,可以采用同一個 Map 來描述。
2. 隱藏類變遷(Map Transition)
因為 JavaScript 是高度動態(tài)的程序設(shè)計語言,對象的成員可以被隨意動態(tài)地添加、刪除甚至修改類型。因此,對象的隱藏類在程序的運行過程中可能會發(fā)生變化,V8 內(nèi)部把這種變化叫隱藏類變遷(Map Transition)。
以上文代碼為例,當 Point function 被聲明時,V8 就會給 Point 創(chuàng)建隱藏類 map0,由于暫時還沒有屬性,因此 map0 為空。當執(zhí)行 this.x= x 時,V8 會創(chuàng)建第二個 Hidden Class map1,map1 是基于 map0,并且描述了屬性 x 在內(nèi)存中的位置,此時 this 對象的 Hidden Class 會通過 Map Transition 變?yōu)?map1。當執(zhí)行 this.y= y 時,會重復前面的操作,一個新的 Hidden Class map2 會被創(chuàng)建,此時 this 對象的 Hidden Class 被更新為 map2。this 對象的 Map 從 map0 變?yōu)?map1,再變成 map2 的過程就叫做 Map Transition。圖一描述了 Point 類對象創(chuàng)建過程中 Map Transition 的過程。
Map Transition 示意圖
3. 類型反饋向量(type feedback vector)
前面已經(jīng)提到 IC 機制的原理是:對于某代碼語句比如 this.x=x,比較上次執(zhí)行到該語句時緩存的 Map 和對象當前的 Map 是否相同,如果相同則執(zhí)行對應(yīng)的 IC-Hit 代碼,反之執(zhí)行 IC-Miss 代碼。那么 V8 是如何組織被緩存的 Map 和 IC-Hit 代碼?以上文代碼為例,V8 會在 Point 函數(shù)對象上添加一個名為 type_feedback_vector 的數(shù)組成員,對于該函數(shù)中的每處可能產(chǎn)生 IC 的代碼,Point 對象中的 type_feedback_vector 會緩存上一次執(zhí)行至該語句時對象的 Map 和對應(yīng)的 IC-Hit 代碼(在 V8 內(nèi)部稱為 IC-Hit Handler)。上文中的 Point 函數(shù)中有兩處可能產(chǎn)生 IC 的語句,this.x= x 和 this.y=y。假設(shè)某次執(zhí)行至 this.x= x 時,對象 this 的 Map 是 map0,執(zhí)行至 this.y= y 時 this 的 Map 是 map1,那么 Point 對象的 type_feedback_vector 數(shù)據(jù)內(nèi)容如下所示:
數(shù)組下標 IC 對應(yīng)的源碼緩存的 Map 和對應(yīng)的 IC-Hit Handler0this.x=x map0, ic-hit handler 1this.y=y map1, ic-hit handler
簡單來說,type_feedback_vector 緩存了 Map 和與之對應(yīng)的 IC-Hit handler,這樣 IC 相關(guān)的邏輯簡化為只需要通過訪問 type_eedback_vector 就可以判斷是否 IC Hit 并執(zhí)行對應(yīng)的 IC-Hit Handler。
4. IC 狀態(tài)機
為了描述 V8 中 IC 狀態(tài)的變化情況,本節(jié)將以狀態(tài)機的形式描述 V8 中最常見 IC 種類的狀態(tài)變化情況。V8 中最常用 的 IC 分為五個狀態(tài),如圖二所示。初始為 uninitialized 狀態(tài),當發(fā)生一次 IC-Miss 時會變?yōu)?pre-monomorphic 態(tài),再次 IC-Miss 會進入 monomorphic 態(tài),如果繼續(xù) IC-Miss,則會進入 polymorphic 狀態(tài)。進入 polymorphic 之后如果繼續(xù) IC-Miss 3 次,則會進入 megamorphic 態(tài),并最終穩(wěn)定在 megamophic 態(tài)。
IC 狀態(tài)機
引例中代碼會涉及到 IC 狀態(tài)機的前三種狀態(tài)。
以 Point 函數(shù)走紅 this.x= x 語句為例,第一次執(zhí)行時,由于 Point.type_feedback_vetor 為空,因此此時會發(fā)生 IC-Miss,并將該處 IC 狀態(tài)從 uninitialized 設(shè)置為 pre-monomorphic,IC-Miss Handler 會分析出此時 this 對象的 Map 中不包含屬性 x,因此會添加成員 x,接著會發(fā)生 Map Transition,即前文提到的 this 對象的隱藏類從 map0 變?yōu)?map1。由于考慮到大部分函數(shù)可能只會被調(diào)用一次,因此 V8 的策略是發(fā)生第一次 IC-Miss 時,并不會緩存此時的 map,也不會產(chǎn)生 IC-Hit handler;
第二次調(diào)用構(gòu)造函數(shù)執(zhí)行 this.x= x 時,由于 Point.type_feedback_vector 仍然為空,因此會發(fā)生第二次 IC-Miss,并將 IC 狀態(tài)修改為 monomorphic,此次 IC-Miss Hanlder 除了發(fā)生 Map Transition 之外,還會編譯生成 IC-Hit Handler,并將 map0 和 IC Hit Handler 緩存到 Point.type_feedback_vector 中。由于此次 IC-Miss Handler 需要編譯 IC-Hit Handler 的操作比較耗時,因此第二次執(zhí)行 this.x= x 是最慢的;
第三次調(diào)用構(gòu)造函數(shù)中 this.x= x 時,發(fā)現(xiàn) Point.type_feedback_vector 不為空,且此時緩存的 map0 與此時 this 對象的 Map 也是一致的,因此會直接調(diào)用 IC-Hit Handler 來添加成員 x 并進行 Map transition。由于此次無需對 map0 進行分析,也無需編譯 IC-Hit Handler,因此此時執(zhí)行效率比前兩次都高。
至此,已經(jīng)解釋清楚為什么 V8 執(zhí)行構(gòu)造函數(shù)時,第二遍最慢而第三遍最快的原因。5. Polymorphic 和 Megamorphic
function f(o) {
return o.x;
}
f({x:1}) //pre-monomorphic
f({x:2}) //monomorphic
f({x:3, y:1}) // polymorphic degree 2
f({x:4, z:1}) // polymorphic degree 3
f({x:5, a:1}) // polymorphic degree 4
f({x:6, b:1}) // megamorphic
上述代碼描述了圖二狀態(tài)機中 polymorphic 態(tài)和 megamophic 態(tài)的兩種情形。上面 3 中提到 type_feedback_vector 會緩存 Map 和 IC-Hit Handler,但是如果 IC 狀態(tài)太多比如到達 megamorphic 態(tài),此時 Map 和 IC-Hit Handler 便不會再緩存在 Point 對象的 feedback_vector 中,而是存儲在固定大小的全局 hashtable 中,如果 IC 態(tài)多于 hashtable 的大小,則會對之前的緩存進行覆蓋。通過上述分析,可以總結(jié)得出不同 IC 態(tài)的性能:如果每次都能在 monomorphic 態(tài) IC-Hit,代碼的運行速度是最快的;在 polymorphic 態(tài) IC-Hit 時,需要對緩存進行線性查找;Megamorphic 是性能最低的 IC-Hit,因為需要每次對 hashtable 進行查找,但是 megamorphic ic hit 性能仍然優(yōu) 于 IC-Miss;
IC-Miss 性能是最差的;
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進一步的了解或閱讀更多相關(guān)文章,請關(guān)注丸趣 TV 行業(yè)資訊頻道,感謝您對丸趣 TV 的支持。