我們知道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)。之所以存在過渡隱藏類是為了在多個對象間能夠共享隱藏類。
這里,注意到xiaoming和lisi兩個對象使用的是同一個構(gòu)造函數(shù),所以它們會共享同一個隱藏類C2。隨后雖然xiaoming和lisi兩個對象都添加了job和email兩個屬性,但由于初始化順序不同,會生成不同的隱藏類。

不同初始化順序的對象,所生成的隱藏類是不一樣的。因此,在實(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-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