我們知道Javascript作為一種動態語言,效能方面與c#,Java之類的靜態語言相比存在著一定的差距。而隨著Web技術的發展,對Javascript的執行效率提出越來越高的要求。為了追求更好的效能,V8引擎借鑑了大量的靜態語言編譯技術來優化引擎的執行效率。比如V8引擎放棄生成中間位元組碼,而是直接從AST(抽象語法樹)生成機器語言。與靜態語言不同, javascript的程式在執行期間需要反覆檢查資料型別。因此,V8引擎中存在兩種機制來優化這個過程。
hidden class 隱藏類
對於動態型別語言來說,由於型別的不確定性,在方法呼叫過程中,語言引擎每次都需要進行動態查詢,這就造成大量的效能消耗,從而降低程式執行的速度。大多數的Javascript 引擎會採用雜湊表的方式來存取屬性和尋找方法。而為了加快物件屬性和方法在記憶體中的查詢速度,V8引擎引入了隱藏類(Hidden Class)的機制,起到給物件分組的作用。在初始化物件的時候,V8引擎會建立一個隱藏類,隨後在程式執行過程中每次增減屬性,就會建立一個新的隱藏類或者查詢之前已經建立好的隱藏類。每個隱藏類都會記錄對應屬性在記憶體中的偏移量,從而在後續再次呼叫的時候能更快地定位到其位置。
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";
複製程式碼
觀察以上程式碼,當初始化Person
物件的時候, 最開始會建立一個C0的隱藏類,該類不帶有任何屬性。隨後在呼叫構造器函式的時候,隨著屬性的增加,引擎會生成C1,C2的過渡隱藏類,隱藏類內部會記錄屬性的偏移量(offset)。之所以存在過渡隱藏類是為了在多個物件間能夠共享隱藏類。
這裡,注意到xiaoming
和lisi
兩個物件使用的是同一個建構函式,所以它們會共享同一個隱藏類C2。隨後雖然xiaoming
和lisi
兩個物件都新增了job
和email
兩個屬性,但由於初始化順序不同,會生成不同的隱藏類。
不同初始化順序的物件,所生成的隱藏類是不一樣的。因此,在實際開發過程中,應該儘量保證屬性初始化的順序一致,這樣生成的隱藏類可以得到共享。同時,儘量在建構函式裡就初始化所有物件成員,減少隱藏類的產生。
inline caching 內聯快取
僅擁有隱藏類似乎還不夠,畢竟引擎在執行過程中還需要查詢隱藏類。為了取得更好的效能,V8引擎加入了內聯快取(Inline Caching)技術來優化執行時查詢物件及其屬性的過程。這項技術其實很古老了,最初是應用在Smalltalk虛擬機器上。核心原理就是在執行過程中,收集型別資訊,從而可以讓引擎在後續執行過程中利用這些型別資訊作出預判。
對於動態查詢優化來說,最簡單的方式是利用快取來保留最常使用的查詢結果。每次呼叫物件上的方法或屬性的時候先查詢快取,如果命中則直接使用快取結果。如果未命中,就查詢隱藏類來獲取結果。內聯快取也是基於這個思想。但是如果想要進一步優化查詢效率,應該怎麼做呢? 考慮到在程式中型別很少發生改變,內聯快取技術會直接將查詢結果寫入呼叫方法中,來避免查詢快取。但是萬一型別在程式執行中途發生變化了怎麼辦?對於這種情況,內聯快取會在直接呼叫之前驗證型別,這些驗證型別的程式碼叫做"前導程式碼"。
var arr = [1, 2, 3, 4];
arr.forEach((item) => console.log(item.toString());
複製程式碼
像上面這段程式碼,數字1在第一次toString()
方法時會發起一次動態查詢,並記錄查詢結果。當後續再呼叫toString
方法時,引擎就能根據上次的記錄直接獲知呼叫點,不再進行動態查詢操作。
再來考慮下面這個情況:
var arr = [1, '2', 3, '4'];
arr.forEach((item) => console.log(item.toString());
複製程式碼
可以看到,呼叫toString
方法的物件型別經常發生改變,這就會導致快取失效。為了防止這種情況發生,V8引擎採用了 polymorphic inline cache (PIC) 技術, 該技術不僅僅只快取最後一次查詢結果,還會快取多次的查詢結果(取決於記錄上限)。
參考資料
blog.sessionstack.com/how-javascr…