V8引擎優(yōu)化機(jī)制之隱藏類和內(nèi)聯(lián)緩存

我們知道Javascript作為一種動態(tài)語言,性能方面與c#,Java之類的靜態(tài)語言相比存在著一定的差距。而隨著Web技術(shù)的發(fā)展,對Javascript的執(zhí)行效率提出越來越高的要求。為了追求更好的性能,V8引擎借鑒了大量的靜態(tài)語言編譯技術(shù)來優(yōu)化引擎的執(zhí)行效率。比如V8引擎放棄生成中間字節(jié)碼,而是直接從AST(抽象語法樹)生成機(jī)器語言。與靜態(tài)語言不同, javascript的程序在執(zhí)行期間需要反復(fù)檢查數(shù)據(jù)類型。因此,V8引擎中存在兩種機(jī)制來優(yōu)化這個過程。

hidden class 隱藏類

對于動態(tài)類型語言來說,由于類型的不確定性,在方法調(diào)用過程中,語言引擎每次都需要進(jìn)行動態(tài)查詢,這就造成大量的性能消耗,從而降低程序運(yùn)行的速度。大多數(shù)的Javascript 引擎會采用哈希表的方式來存取屬性和尋找方法。而為了加快對象屬性和方法在內(nèi)存中的查找速度,V8引擎引入了隱藏類(Hidden Class)的機(jī)制,起到給對象分組的作用。在初始化對象的時候,V8引擎會創(chuàng)建一個隱藏類,隨后在程序運(yùn)行過程中每次增減屬性,就會創(chuàng)建一個新的隱藏類或者查找之前已經(jīng)創(chuàng)建好的隱藏類。每個隱藏類都會記錄對應(yīng)屬性在內(nèi)存中的偏移量,從而在后續(xù)再次調(diào)用的時候能更快地定位到其位置。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

var xiaoming = new Person("xiaoming", 32);
var lisi = new Person("lisi", 20);

xiaoming.email = "xiaoming@qq.com";
xiaoming.job = "teacher";

lisi.job = "chef";
lisi.email = "lisi@qq.com";

觀察以上代碼,當(dāng)初始化Person對象的時候, 最開始會創(chuàng)建一個C0的隱藏類,該類不帶有任何屬性。隨后在調(diào)用構(gòu)造器函數(shù)的時候,隨著屬性的增加,引擎會生成C1,C2的過渡隱藏類,隱藏類內(nèi)部會記錄屬性的偏移量(offset)。之所以存在過渡隱藏類是為了在多個對象間能夠共享隱藏類。

這里,注意到xiaominglisi兩個對象使用的是同一個構(gòu)造函數(shù),所以它們會共享同一個隱藏類C2。隨后雖然xiaominglisi兩個對象都添加了jobemail兩個屬性,但由于初始化順序不同,會生成不同的隱藏類。

hiddenClass.png

不同初始化順序的對象,所生成的隱藏類是不一樣的。因此,在實(shí)際開發(fā)過程中,應(yīng)該盡量保證屬性初始化的順序一致,這樣生成的隱藏類可以得到共享。同時,盡量在構(gòu)造函數(shù)里就初始化所有對象成員,減少隱藏類的產(chǎn)生。

inline caching 內(nèi)聯(lián)緩存

僅擁有隱藏類似乎還不夠,畢竟引擎在執(zhí)行過程中還需要查找隱藏類。為了取得更好的性能,V8引擎加入了內(nèi)聯(lián)緩存(Inline Caching)技術(shù)來優(yōu)化運(yùn)行時查找對象及其屬性的過程。這項技術(shù)其實(shí)很古老了,最初是應(yīng)用在Smalltalk虛擬機(jī)上。核心原理就是在運(yùn)行過程中,收集類型信息,從而可以讓引擎在后續(xù)運(yùn)行過程中利用這些類型信息作出預(yù)判。

對于動態(tài)查詢優(yōu)化來說,最簡單的方式是利用緩存來保留最常使用的查詢結(jié)果。每次調(diào)用對象上的方法或?qū)傩缘臅r候先查詢緩存,如果命中則直接使用緩存結(jié)果。如果未命中,就查詢隱藏類來獲取結(jié)果。內(nèi)聯(lián)緩存也是基于這個思想。但是如果想要進(jìn)一步優(yōu)化查詢效率,應(yīng)該怎么做呢? 考慮到在程序中類型很少發(fā)生改變,內(nèi)聯(lián)緩存技術(shù)會直接將查詢結(jié)果寫入調(diào)用方法中,來避免查詢緩存。但是萬一類型在程序執(zhí)行中途發(fā)生變化了怎么辦?對于這種情況,內(nèi)聯(lián)緩存會在直接調(diào)用之前驗證類型,這些驗證類型的代碼叫做"前導(dǎo)代碼"。

var arr = [1, 2, 3, 4];
arr.forEach((item) => console.log(item.toString());

像上面這段代碼,數(shù)字1在第一次toString()方法時會發(fā)起一次動態(tài)查詢,并記錄查詢結(jié)果。當(dāng)后續(xù)再調(diào)用toString方法時,引擎就能根據(jù)上次的記錄直接獲知調(diào)用點(diǎn),不再進(jìn)行動態(tài)查詢操作。

再來考慮下面這個情況:

var arr = [1, '2', 3, '4'];
arr.forEach((item) => console.log(item.toString());

可以看到,調(diào)用toString方法的對象類型經(jīng)常發(fā)生改變,這就會導(dǎo)致緩存失效。為了防止這種情況發(fā)生,V8引擎采用了 polymorphic inline cache (PIC) 技術(shù), 該技術(shù)不僅僅只緩存最后一次查詢結(jié)果,還會緩存多次的查詢結(jié)果(取決于記錄上限)。

參考資料

https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e

https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

https://github.com/sq/JSIL/wiki/Optimizing-dynamic-JavaScript-with-inline-caches

https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JavaScript絕對是最火的編程語言之一,一直具有很大的用戶群,隨著在服務(wù)端的使用(NodeJs),更是爆發(fā)了...
    不去解釋閱讀 2,521評論 1 16
  • V8的前世今生 V8是JavaScript渲染引擎,第一個版本隨著Chrome的發(fā)布而發(fā)布(具體時間為2008年9...
    燕京博士閱讀 2,965評論 1 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,664評論 1 32
  • 關(guān)于前端性能優(yōu)化問題詳解 出處:http://segmentfault.com/blogs 前端性能優(yōu)化指南 AJ...
    bennnnn閱讀 1,720評論 2 4
  • 沿著羊腸小道,穿過一口碧綠的池塘和路邊的兩排整齊的柳樹,一直走到盡頭。 映入眼簾的是幾棟規(guī)格不一的小洋房,磚紅色的...
    草莓醬的烤雞腿閱讀 305評論 0 0

友情鏈接更多精彩內(nèi)容