想調用一個方法很容易,直接代碼調用就行,這人人都會。其次呢,還可以使用反射。不過通過反射調用的性能會遠遠低于直接調用――至少從絕對時間上來看的確是這樣。雖然這是個眾所周知的現象,我們還是來寫個程序來驗證一下。比如我們現在新建一個Console應用程序,編寫一個最簡單的Call方法。
public void Call(object o1, object o2, object o3) { }
}
Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (int i = 0; i < times; i++)
{
program.Call(parameters[0], parameters[1], parameters[2]);
}
watch1.Stop();
Console.WriteLine(watch1.Elapsed + " (Directly invoke)");
MethodInfo methodInfo = typeof(Program).GetMethod("Call");
Stopwatch watch2 = new Stopwatch();
watch2.Start();
for (int i = 0; i < times; i++)
{
methodInfo.Invoke(program, parameters);
}
watch2.Stop();
Console.WriteLine(watch2.Elapsed + " (Reflection invoke)");
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
通過各調用一百萬次所花時間來看,兩者在性能上具有數量級的差距。因此,很多框架在必須利用到反射的場景中,都會設法使用一些較高級的替代方案來改善性能。例如,使用CodeDom生成代碼并動態編譯,或者使用Emit來直接編寫IL。不過自從.NET 3.5發布了Expression相關的新特性,我們在以上的情況下又有了更方便并直觀的解決方案。
了解Expression相關特性的朋友可能知道,System.Linq.Expressions.Expression<TDelegate>類型的對象在調用了它了Compile方法之后將得到一個TDelegate類型的委托對象,而調用一個委托對象與直接調用一個方法的性能開銷相差無幾。那么對于上面的情況,我們又該得到什么樣的Delegate對象呢?為了使解決方案足夠通用,我們必須將各種簽名的方法統一至同樣的委托類型中,如下:
Action<object, object[]> action = exp.Compile();
return (instance, parameters) =>
{
action(instance, parameters);
return null;
};
}
public Func<object, object[], object> GetDelegate()
{
Expression<Func<object, object[], object>> exp = (instance, parameters) =>
((Program)instance).Call(parameters[0], parameters[1]);
return exp.Compile();
}
public DynamicMethodExecutor(MethodInfo methodInfo)
{
this.m_execute = this.GetExecuteDelegate(methodInfo);
}
public object Execute(object instance, object[] parameters)
{
return this.m_execute(instance, parameters);
}
private Func<object, object[], object> GetExecuteDelegate(MethodInfo methodInfo)
{
// parameters to execute
ParameterExpression instanceParameter =
Expression.Parameter(typeof(object), "instance");
ParameterExpression parametersParameter =
Expression.Parameter(typeof(object[]), "parameters");
// build parameter list
List<Expression> parameterExpressions = new List<Expression>();
ParameterInfo[] paramInfos = methodInfo.GetParameters();
for (int i = 0; i < paramInfos.Length; i++)
{
// (Ti)parameters[i]
BinaryExpression valueObj = Expression.ArrayIndex(
parametersParameter, Expression.Constant(i));
UnaryExpression valueCast = Expression.Convert(
valueObj, paramInfos[i].ParameterType);
parameterExpressions.Add(valueCast);
}
// non-instance for static method, or ((TInstance)instance)
Expression instanceCast = methodInfo.IsStatic ? null :
Expression.Convert(instanceParameter, methodInfo.ReflectedType);
// static invoke or ((TInstance)instance).Method
MethodCallExpression methodCall = Expression.Call(
instanceCast, methodInfo, parameterExpressions);
// ((TInstance)instance).Method((T0)parameters[0], (T1)parameters[1], ...)
if (methodCall.Type == typeof(void))
{
Expression<Action<object, object[]>> lambda =
Expression.Lambda<Action<object, object[]>>(
methodCall, instanceParameter, parametersParameter);
Action<object, object[]> execute = lambda.Compile();
return (instance, parameters) =>
{
execute(instance, parameters);
return null;
};
}
else
{
UnaryExpression castMethodCall = Expression.Convert(
methodCall, typeof(object));
Expression<Func<object, object[], object>> lambda =
Expression.Lambda<Func<object, object[], object>>(
castMethodCall, instanceParameter, parametersParameter);
return lambda.Compile();
}
}
}
DynamicMethodExecutor的關鍵就在于GetExecuteDelegate方法中構造Expression Tree的邏輯。如果您對于一個Expression Tree的結構不太了解的話,不妨嘗試一下使用Expression Tree Visualizer 來對一個現成的Expression Tree進行觀察和分析。我們將一個MethodInfo對象傳入DynamicMethodExecutor的構造函數之后,就能將各組不同的實例對象和參數對象數組傳入Execute進行執行。這一切就像使用反射來進行調用一般,不過它的性能就有了明顯的提高。例如我們添加更多的測試代碼:
現在的執行結果則是:
補充
木野狐兄在評論中引用了Code Project的文章《A General Fast Method Invoker》,其中通過Emit構建了FastInvokeHandler委托對象(其簽名與Func<object, object[], object>完全相同)的調用效率似乎較“方法直接”調用的性能更高(雖然從原文示例看來并非如此)。事實上FastInvokeHandler其內部實現與DynamicMethodExecutor完全相同,居然有如此令人不可思議的表現實在讓人嘖嘖稱奇。我猜測,FastInvokeHandler與DynamicMethodExecutor的性能優勢可能體現在以下幾個方面:
1.范型委托類型的執行性能較非范型委托類型略低(求證)。
2.多了一次Execute方法調用,損失部分性能。
3.生成的IL代碼更為短小緊湊。
4.木野狐兄沒有使用Release模式編譯。:P
不知道是否有對此感興趣的朋友能夠再做一個測試,不過請注意此類性能測試一定需要在Release編譯下進行(這點很容易被忽視),否則意義其實不大。
此外,我還想強調的就是,本篇文章進行是純技術上的比較,并非在引導大家追求點滴性能上的優化。有時候看到一些關于比較for或foreach性能優劣的文章讓許多朋友都糾結與此,甚至搞得面紅耳赤,我總會覺得有些無可奈何。其實從理論上來說,提高性能的方式有許許多多,記得當時在大學里學習Introduction to Computer System這門課時得一個作業就是為一段C程序作性能優化,當時用到不少手段,例如內聯方法調用以減少CPU指令調用次數、調整循環嵌套順序以提高CPU緩存命中率,將一些代碼使用內嵌ASM替換等等,可謂“無所不用其極”,大家都在為幾個時鐘周期的性能提高而發奮圖強歡呼雀躍……
那是理論,是在學習。但是在實際運用中,我們還必須正確對待學到的理論知識。我經常說的一句話是:“任何應用程序都會有其性能瓶頸,只有從性能瓶頸著手才能做到事半功倍的結果。”例如,普通Web應用的性能瓶頸往往在外部IO(尤其是數據庫讀寫),要真正提高性能必須從此入手(例如數據庫調優,更好的緩存設計)。正因如此,開發一個高性能的Web應用程序的關鍵不會在語言或語言運行環境上,.NET、RoR、PHP、Java等等在這一領域都表現良好。
新聞熱點
疑難解答