讓我們談談JavaScript單態(二)
未討論的:
在撰寫本文時,故意略去了一些實現細節,以免它在內容上過去寬泛。
形狀
我們沒有討論對象的形狀(即隱藏類)是如何表示,如何得到的以及如何和對象綁定在一起的。可以看下過去寫的這篇 post on inline caches 以及我過去的一些會談,比如AWP2014 來獲得一些基本的瞭解。
這裏需要你知道的一件重要的事是:JavaScript VMs中的形狀是一種啓發式的近似(實際並不存在,但類比),它試圖動態的發掘出程序中隱藏的靜態結構。並且有時候,對於人來說某些對象具有相似的形狀,但是VMs來說則不然。
function A() { this.x = 1 }
function B() { this.x = 1 }
var a = new A,
b = new B,
c = { x: 1 },
d = { x: 1, y: 1 }
delete d.y
// a, b, c, d 在V8中都具有不同的形狀。
JavaScript對象的易擴展性,使得創建意外的多態變得異常容易
function A() {
this.x = 1;
}
var a = new A(), b = new A(); // same shape
if (something) {
a.y = 2; // shape of a no longer matches b.
}
故意的多態
即使你使用的編程語言只允許你創建固定類型的對象(Java, C#, Dart, C++, etc),你仍然可以實現多態代碼:
interface Base {
void doX();
}
class A implements Base {
void doX() { }
}
class B implements Base {
void doX() { }
}
void handle(Base obj) {
obj.foo();
}
handle(new A());
handle(new B());
// obj.foo() callsite is polymorphic
能夠針對接口編程,並且通過繼承實現不同的對象行爲是一種很重要的抽象機制。靜態類型編程語言的多態實現與上面我們說的具有相似的性能表現。
並非所有的緩存都是一樣的
記住並非所有的緩存都是基於(對象)形狀的,以及它們的容量都比較低。例如, 與函數調用關聯的緩存可能是未初始化,單態或者megamorphic態的,但不存在位於它們中間的多態狀態。如果去緩存函數的形狀,就會與函數的調用無關,所以對於函數會去緩存函數的調用目標—即函數本身。
function inv(cb) {
return cb(0)
}
function F(v) { return v }
function G(v) { return v + 1 }
inv(F)
// inline cache is monomorpic, points to F
inv(G)
// inline cache is megamorphic
如果在cb(…)調用處的內聯緩存還是單態的時候去優化inv,這時候優化器可會內聯這個調用(對於小的,並且調用頻繁的函數這個技術具有重要的作用)(譯註:即內聯函數體)。當這個緩存是megamorphic 態,優化器就無法內聯任何函數了(它不知道該內聯哪一個,目標存在多個),轉而在IR中留下一個通用的調用操作。
還有一種函數調用情況,就是o.m(…) 這種和屬性訪問相似的形式。這種函數調用情況下,內聯緩存會存在單態和megamorphic態中間的多態形式。V8能夠和屬性訪問相同的方式構建IR:選擇決策樹或者一個位於內聯函數體之前的多態類型守衛。然而這裏有一個限制:V8要能夠內聯函數調用,就需要把函數看作某種形狀,就像對象形狀一樣。
(譯註:實際上o.m(…)會編譯成兩個IC,一個LoadIC負責屬性加載,另外一個CallIC 負責這個消息的接受者)
function inv(o) {
return o.cb(0)
}
var f = {
cb: function F(v) { return v },
};
var g = {
cb: function G(v) { return v + 1 },
};
inv(f)
inv(f)
// here inline cache is monomorpic, have seen only objects with
// a shape like f.
inv(g)
// here inline cache is polymorphic, seen objects with two different
// shapes: like f and like g
你可能會奇怪上面的f和g擁有不同的形狀(譯註:因爲會類比上面單態的例子)。出現這種情況是因爲當我們把函數分配給一個屬性時,V8會嘗試(如果可能)將函數附加到對象的形狀是,而不是將其保存在對象說(譯註:就像保存屬性的值一樣)。在這個例子中,f的形狀是這樣:{cb: F},即形狀本身指向閉包。在我們之前的例子中,我們所說的形狀,只是通過一定偏移量去訪問屬性的存在,並同時獲取到屬性的值。這使得V8的形狀類似於像Java或者C++這樣的語言中的類,其中類本質上一組子段和方法。
當然,如果稍後你用不同的函數去覆蓋函數屬性,V8會切換它的形狀像這樣:
var f = {
cb: function F(v) { return v },
};
// Shape of f is {cb: F}
f.cb = function H(v) { return v - 1 }
// Shape of f is {cb: *}
總的來說,V8如何構建並維護形狀(隱藏類)這個話題本身值得好好討論研究。
屬性的路徑
在此刻,與屬性訪問關聯的內聯緩存似乎是一個將形狀映射到屬性偏移的字典上,就像Dictionary
// pseudo-code reimagining o = { x: 1 }
var o = {
get x () {
return $LoadByOffset(this, offset_of_x)
},
set x (value) {
$StoreByOffset(this, offset_of_x, value)
}
// both getter and setter are generated internally by VM
// and are invisible to normal JS code.
};
$StoreByOffset(this, offset_of_x, 1)
根據這一觀察結果,很明顯IC應該更類似Dictionary
預單態狀態:
在V8中,還存在一種介於未初始化和單態之間的預單態(premonomorphic )狀態。這是爲了避免只執行一次的內聯緩存樁代碼。這裏不討論這個,因爲這是一個有點模糊的實現細節(譯註:想了解可以參考擴展閱讀二,PIC)
最後關於性能的建議
最好的性能建議隱藏類 Dale Carnegie’s的書名中:“如何停止擔憂並開始生活”(譯註:原文爲How to Stop Worrying and Start Living)
事實上,擔心多態性通常是徒勞的。你應該在你真實的項目中進行基準測試,對有問題的熱點進行分析,如何它和JS有關–去看看編譯器生成的IR。
如果你看到了諸如XYZGeneric 的IR指令,或者任何標有紅色的[*] (又名可以成爲一切)的標記,然後(只有這樣)你應該開始擔心多態或什麼的。
擴展閱讀:
一,V8中的多態內聯緩存PIC
二,Explaining JavaScript VMs in JavaScript - Inline Caches
三, Optimizing Dynamically-Typed Object-Oriented Languages WithPolymorphic Inline Caches