Groovy 1.6引入了Call Site優化。Call Site優化實際上就是方法選擇的cache。
方法選擇
在靜態語言(如Java)中,方法調用的綁定是在編譯期完成的(不完全是這樣,如虛函數,但總的來說,靜態語言的方法調用是非常高效的)。而在動態語言(如Groovy)中,調用的方法是在運行時選擇的。這也是造成動態語言比靜態語言慢的重要原因之一。
舉個例子來說明,譬如要調用“a.call(1)”。
如果是Java的話,在編譯期就會選擇好調用的方法,在這個例子中,就是選擇a對象聲明的類(注意,不是a對象真正的類,因為真正的類要到運行時才能知道)中,名字為call、有一個參數、參數類型為int的方法(實際情況要復雜很多,譬如還要考慮boxing和變參等情況),如果找不到的話則編譯不通過,否則進行方法綁定。反匯編這個方法調用的話可以看到如“invokevirtual #4; //Method call:(I)V”的指令,表明方法是綁定好的,包括方法名字“call”,參數類型“I”(int),返回值“V”(void)。
如果是Groovy的話,這些都是由Groovy運行時完成的,Groovy對代碼進行編譯時並不會檢查到底有沒有一個方法能匹配這個調用。用 Groovy 1.5.7進行編譯,再反編譯為Java代碼之後,可以看到如 “ScriptBytecodeAdapter.invokeMethodN(class1, a, "call", new Object[] { new Integer(1) })”的語句,由此看出Groovy在編譯時並沒有真正的選擇調用的方法,而是交由運行時決定。
Call Site
根據wikipedia的定義(http://en.wikipedia.org/wiki/Call_site),Call Site是一行方法的調用,譬如:
a = sqr(b);
c = sqr(b);
是兩個Call Site。
Call Site優化
在Groovy 1.6之前,對於同一個Call Site來說,調用該方法n次,則會進行n次的方法選擇,譬如:
for (i in 1..3) {
a.call(i)
}
for (i in 1..3) { a.call(i) }
這裡,Groovy對call方法就進行了3次的選擇,即使3次選擇的結果都是一樣的。
Groovy 1.6引入的Call Site優化,則是把同一個Call Site的方法選擇結果緩存起來,如果下一次調用時的參數類型一樣,則調用該緩存起來的方法,否則重新選擇。這就是Call Site優化的基本思想。
代碼分析
考慮以下的Groovy代碼:
class A {
def a() {}
def b() {}
def b(int i) {}
}
class B {
def a = new A()
def c() {
a.a()
d()
}
def d() {
a.a()
a.b()
a.b(1)
}
}
我們先用Groovy 1.6對這段代碼進行編譯,然後再用jad對編譯後的class文件進行反編譯。因為A類中的方法都是空的,所以我們只看B類的反編譯結果。下面是B類中的c()和d()方法:
public Object c()
{
CallSite acallsite[] = $getCallSiteArray();
acallsite[1].call(a);
return acallsite[2].callCurrent(this);
}
public Object d()
{
CallSite acallsite[] = $getCallSiteArray();
acallsite[3].call(a);
acallsite[4].call(a);
return acallsite[5].call(a, $const$0); // $const$0就是常量1
}
我們來看看$getCallSiteArray():
private static CallSiteArray $createCallSiteArray()
{
return new CallSiteArray($ownClass, new String[] {
"<$constructor$>", "a", "d", "a", "b", "b" // 每個Call Site的方法名字
});
}
private static CallSite[] $getCallSiteArray()
{
CallSiteArray callsitearray;
if($callSiteArray == null || (callsitearray = (CallSiteArray)$callSiteArray.get()) == null)
{
callsitearray = $createCallSiteArray();
$callSiteArray = new SoftReference(callsitearray);
}
return callsitearray.array;
}
$getCallSiteArray()實際上就是對$callSiteArray的lazy創建。
我們可以看到,“acallsite[1].call(a);”就是對方法名為"a"的CallSite進行調用,而“acallsite[2].callCurrent(this);”則是對方法名為“d”的CallSite進行調用,如此類推。
我們再來看看CallSiteArray的構造函數裡做些什麼:
public CallSiteArray(Class owner, String [] names) {
this.owner = owner;
array = new CallSite[names.length];
for (int i = 0; i < array.length; i++) {
array[i] = new AbstractCallSite(this, i, names[i]);
}
}
所以,第一次調用“acallsite[1].call(a);“時,就是調用AbstractCallSite類的call方法。下面是該方法的代碼:
public Object call(Object receiver, Object[] args) throws Throwable {
return CallSiteArray.defaultCall(this, receiver, args);
}
再看看CallSiteArray.defaultCall()的代碼:
public static Object defaultCall(CallSite callSite, Object receiver, Object[] args) throws Throwable {
return createCallSite(callSite, receiver, args).call(receiver, args);
}
private static CallSite createCallSite(CallSite callSite, Object receiver, Object[] args) {
CallSite site;
if (receiver == null)
return new NullCallSite(callSite);
if (receiver instanceof Class)
site = createCallStaticSite(callSite, (Class) receiver, args);
else if (receiver instanceof GroovyObject) {
site = createPogoSite(callSite, receiver, args); // 我們只考慮這種情況
} else {
site = createPojoSite(callSite, receiver, args);
}
replaceCallSite(callSite, site); // 替換CallSite
return site;
}
private static void replaceCallSite(CallSite oldSite, CallSite newSite) {
oldSite.getArray().array [oldSite.getIndex()] = newSite;
}
可以看到createCallSite()最後通過調用replaceCallSite()把舊的CallSite替換為新的CallSite,因此第二次調用“acallsite[1].call(a);”時就是直接調用新的CallSite,也就是說該CallSite被緩存起來了。
我們在這裡只考慮POGO的情況,即createPogoSite()方法。而POJO的情況稍微復雜一點,因為涉及到POJO per-instance metaclass的情況(我將在下一篇文章中分析它的實現)。下面是createPogoSite()的代碼:
private static CallSite createPogoSite(CallSite callSite, Object receiver, Object[] args) {
if (receiver instanceof GroovyInterceptable)
return new PogoInterceptableSite(callSite);
MetaClass metaClass = ((GroovyObject)receiver).getMetaClass();
if (metaClass instanceof MetaClassImpl) {
return ((MetaClassImpl)metaClass).createPogoCallSite(callSite, args); // 我們只考慮這種情況
}
return new PogoMetaClassSite(callSite, metaClass);
}
我們只考慮對象的metaclass是MetaClassImpl的情況(這也是Groovy對象的默認情況)。下面是MetaClassImpl.createPogoCallSite()的代碼:
public CallSite createPogoCallSite(CallSite site, Object[] args) {
if (site.getUsage().get() == 0 && !(this instanceof AdaptingMetaClass)) {
Class [] params = MetaClassHelper.convertToTypeArray(args); // 獲取參數的類型
MetaMethod metaMethod = getMethodWithCachingInternal(theClass, site, params); // 選擇方法
if (metaMethod != null)
return PogoMetaMethodSite.createPogoMetaMethodSite(site, this, metaMethod, params, args); // 如果找到匹配的方法,則創建一個PogoMetaMethodSite,並把找到的方法綁定其中
}
return new PogoMetaClassSite(site, this); //否則創建一個PogoMetaClassSite
}
PogoMetaMethodSite.createPogoMetaMethodSite()就是用來根據不同的情況創建PogoMetaMethodSite或它的子類的一個實例。我們最後來看看PogoMetaMethodSite.call()方法:
public Object call(Object receiver, Object[] args) throws Throwable {
if(checkCall(receiver, args)) { // 如果參數類型相同,則調用綁定的方法
try {
return invoke(receiver,args); // 調用綁定的方法
} catch (GroovyRuntimeException gre) {
throw ScriptBytecodeAdapter.unwrap(gre);
}
} else { // 否則創建新的CallSite,即再次進行方法查找
return CallSiteArray.defaultCall(this, receiver, args);
}
}
protected boolean checkCall(Object receiver, Object[] args) {
try {
return usage.get() == 0
&& ((GroovyObject)receiver).getMetaClass() == metaClass // metaClass still be valid
&& MetaClassHelper.sameClasses(params, args); // 檢查參數類型是否一樣
}
catch (NullPointerException e) {
if (receiver == null)
return false;
throw e;
}
catch (ClassCastException e) {
if (!(receiver instanceof GroovyObject))
return false;
throw e;
}
}
最後,我們來再次總結這個過程:
第一次調用“acallsite[1].call(a)“時,通過CallSiteArray.createCallSite()方法創建了 PogoMetaMethodSite類的一個新CallSite,並把默認的AbstractCallSite覆蓋掉。在創建 PogoMetaMethodSite的過程中,將進行方法的選擇,並把找到的方法綁定到PogoMetaMethodSite中。最後就是調用該方法:
當第二次調用“acallsite[1].call(a)“時,就是直接調用PogoMetaMethodSite.call(),這時候 PogoMetaMethodSite.call()就會檢查傳入的參數類型是否與綁定的方法(即上次找到的方法)的參數類型相同,相同則調用該綁定的方法,否則將再次調用CallSiteArray.createCallSite()方法,創建一個新的CallSite對象,並重新進行方法選擇。
除了普通的方法調用的情況外,還有調用當前對象方法、獲取/設置屬性、調用構造函數、調用靜態函數的情況,在此不再做詳細分析,有興趣的可以直接查閱Groovy的源代碼。
以上分析有不當之處敬請指出,謝謝大家的閱讀。