C#中方法的直接調(diào)用、反射調(diào)用與Lambda表達(dá)式調(diào)用對(duì)比
想調(diào)用一個(gè)方法很容易,直接代碼調(diào)用就行,這人人都會(huì)。其次呢,還可以使用反射。不過(guò)通過(guò)反射調(diào)用的性能會(huì)遠(yuǎn)遠(yuǎn)低于直接調(diào)用——至少?gòu)慕^對(duì)時(shí)間上來(lái)看的確是這樣。雖然這是個(gè)眾所周知的現(xiàn)象,我們還是來(lái)寫個(gè)程序來(lái)驗(yàn)證一下。比如我們現(xiàn)在新建一個(gè)Console應(yīng)用程序,編寫一個(gè)最簡(jiǎn)單的Call方法。
class Program
{
static void Main(string[] args)
{
}
public void Call(object o1, object o2, object o3) { }
}
Call方法接受三個(gè)object參數(shù)卻沒(méi)有任何實(shí)現(xiàn),這樣我們就可以讓測(cè)試專注于方法調(diào)用,而并非方法實(shí)現(xiàn)本身。于是我們開(kāi)始編寫測(cè)試代碼,比較一下方法的直接調(diào)用與反射調(diào)用的性能差距:
static void Main(string[] args)
{
int times = 1000000;
Program program = new Program();
object[] parameters = new object[] { new object(), new object(), new object() };
program.Call(null, null, null); // force JIT-compile
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();
}
執(zhí)行結(jié)果如下:
00:00:00.0119041 (Directly invoke)
00:00:04.5527141 (Reflection invoke)
Press any key to continue...
通過(guò)各調(diào)用一百萬(wàn)次所花時(shí)間來(lái)看,兩者在性能上具有數(shù)量級(jí)的差距。因此,很多框架在必須利用到反射的場(chǎng)景中,都會(huì)設(shè)法使用一些較高級(jí)的替代方案來(lái)改善性能。例如,使用CodeDom生成代碼并動(dòng)態(tài)編譯,或者使用Emit來(lái)直接編寫IL。不過(guò)自從.NET 3.5發(fā)布了Expression相關(guān)的新特性,我們?cè)谝陨系那闆r下又有了更方便并直觀的解決方案。
了解Expression相關(guān)特性的朋友可能知道,System.Linq.Expressions.Expression<TDelegate>類型的對(duì)象在調(diào)用了它了Compile方法之后將得到一個(gè)TDelegate類型的委托對(duì)象,而調(diào)用一個(gè)委托對(duì)象與直接調(diào)用一個(gè)方法的性能開(kāi)銷相差無(wú)幾。那么對(duì)于上面的情況,我們又該得到什么樣的Delegate對(duì)象呢?為了使解決方案足夠通用,我們必須將各種簽名的方法統(tǒng)一至同樣的委托類型中,如下:
public Func<object, object[], object> GetVoidDelegate()
{
Expression<Action<object, object[]>> exp = (instance, parameters) =>
((Program)instance).Call(parameters[0], parameters[1], parameters[2]);
Action<object, object[]> action = exp.Compile();
return (instance, parameters) =>
{
action(instance, parameters);
return null;
};
}
如上,我們就得到了一個(gè)Func<object, object[], object>類型的委托,這意味它接受一個(gè)object類型與object[]類型的參數(shù),以及返回一個(gè)object類型的結(jié)果——等等,朋友們有沒(méi)有發(fā)現(xiàn),這個(gè)簽名與MethodInfo類型的Invoke方法完全一致?不過(guò)可喜可賀的是,我們現(xiàn)在調(diào)用這個(gè)委托的性能遠(yuǎn)高于通過(guò)反射來(lái)調(diào)用了。那么對(duì)于有返回值的方法呢?那構(gòu)造一個(gè)委托對(duì)象就更方便了:
public int Call(object o1, object o2) { return 0; }
public Func<object, object[], object> GetDelegate()
{
Expression<Func<object, object[], object>> exp = (instance, parameters) =>
((Program)instance).Call(parameters[0], parameters[1]);
return exp.Compile();
}
至此,我想朋友們也已經(jīng)能夠輕松得出調(diào)用靜態(tài)方法的委托構(gòu)造方式了??梢?jiàn),這個(gè)解決方案的關(guān)鍵在于構(gòu)造一個(gè)合適的Expression<TDelegate>,那么我們現(xiàn)在就來(lái)編寫一個(gè)DynamicExecuter類來(lái)作為一個(gè)較為完整的解決方案:
public class DynamicMethodExecutor
{
private Func<object, object[], object> m_execute;
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的關(guān)鍵就在于GetExecuteDelegate方法中構(gòu)造Expression Tree的邏輯。如果您對(duì)于一個(gè)Expression Tree的結(jié)構(gòu)不太了解的話,不妨嘗試一下使用Expression Tree Visualizer 來(lái)對(duì)一個(gè)現(xiàn)成的Expression Tree進(jìn)行觀察和分析。我們將一個(gè)MethodInfo對(duì)象傳入DynamicMethodExecutor的構(gòu)造函數(shù)之后,就能將各組不同的實(shí)例對(duì)象和參數(shù)對(duì)象數(shù)組傳入Execute進(jìn)行執(zhí)行。這一切就像使用反射來(lái)進(jìn)行調(diào)用一般,不過(guò)它的性能就有了明顯的提高。例如我們添加更多的測(cè)試代碼:
DynamicMethodExecutor executor = new DynamicMethodExecutor(methodInfo);
Stopwatch watch3 = new Stopwatch();
watch3.Start();
for (int i = 0; i < times; i++)
{
executor.Execute(program, parameters);
}
watch3.Stop();
Console.WriteLine(watch3.Elapsed + " (Dynamic executor)");
現(xiàn)在的執(zhí)行結(jié)果則是:
00:00:00.0125539 (Directly invoke)
00:00:04.5349626 (Reflection invoke)
00:00:00.0322555 (Dynamic executor)
Press any key to continue...
事實(shí)上,Expression<TDelegate>類型的Compile方法正是使用Emit來(lái)生成委托對(duì)象。不過(guò)現(xiàn)在我們已經(jīng)無(wú)需將目光放在更低端的IL上,只要使用高端的API來(lái)進(jìn)行Expression Tree的構(gòu)造,這無(wú)疑是一種進(jìn)步。不過(guò)這種方法也有一定局限性,例如我們只能對(duì)公有方法進(jìn)行調(diào)用,并且包含out/ref參數(shù)的方法,或者除了方法外的其他類型成員,我們就無(wú)法如上例般愜意地編寫代碼了。
補(bǔ)充
木野狐兄在評(píng)論中引用了Code Project的文章《A General Fast Method Invoker》,其中通過(guò)Emit構(gòu)建了FastInvokeHandler委托對(duì)象(其簽名與Func<object, object[], object>完全相同)的調(diào)用效率似乎較“方法直接”調(diào)用的性能更高(雖然從原文示例看來(lái)并非如此)。事實(shí)上FastInvokeHandler其內(nèi)部實(shí)現(xiàn)與DynamicMethodExecutor完全相同,居然有如此令人不可思議的表現(xiàn)實(shí)在讓人嘖嘖稱奇。我猜測(cè),F(xiàn)astInvokeHandler與DynamicMethodExecutor的性能優(yōu)勢(shì)可能體現(xiàn)在以下幾個(gè)方面:
1.范型委托類型的執(zhí)行性能較非范型委托類型略低(求證)。
2.多了一次Execute方法調(diào)用,損失部分性能。
3.生成的IL代碼更為短小緊湊。
4.木野狐兄沒(méi)有使用Release模式編譯。:P
不知道是否有對(duì)此感興趣的朋友能夠再做一個(gè)測(cè)試,不過(guò)請(qǐng)注意此類性能測(cè)試一定需要在Release編譯下進(jìn)行(這點(diǎn)很容易被忽視),否則意義其實(shí)不大。
此外,我還想強(qiáng)調(diào)的就是,本篇文章進(jìn)行是純技術(shù)上的比較,并非在引導(dǎo)大家追求點(diǎn)滴性能上的優(yōu)化。有時(shí)候看到一些關(guān)于比較for或foreach性能優(yōu)劣的文章讓許多朋友都糾結(jié)與此,甚至搞得面紅耳赤,我總會(huì)覺(jué)得有些無(wú)可奈何。其實(shí)從理論上來(lái)說(shuō),提高性能的方式有許許多多,記得當(dāng)時(shí)在大學(xué)里學(xué)習(xí)Introduction to Computer System這門課時(shí)得一個(gè)作業(yè)就是為一段C程序作性能優(yōu)化,當(dāng)時(shí)用到不少手段,例如內(nèi)聯(lián)方法調(diào)用以減少CPU指令調(diào)用次數(shù)、調(diào)整循環(huán)嵌套順序以提高CPU緩存命中率,將一些代碼使用內(nèi)嵌ASM替換等等,可謂“無(wú)所不用其極”,大家都在為幾個(gè)時(shí)鐘周期的性能提高而發(fā)奮圖強(qiáng)歡呼雀躍……
那是理論,是在學(xué)習(xí)。但是在實(shí)際運(yùn)用中,我們還必須正確對(duì)待學(xué)到的理論知識(shí)。我經(jīng)常說(shuō)的一句話是:“任何應(yīng)用程序都會(huì)有其性能瓶頸,只有從性能瓶頸著手才能做到事半功倍的結(jié)果。”例如,普通Web應(yīng)用的性能瓶頸往往在外部IO(尤其是數(shù)據(jù)庫(kù)讀寫),要真正提高性能必須從此入手(例如數(shù)據(jù)庫(kù)調(diào)優(yōu),更好的緩存設(shè)計(jì))。正因如此,開(kāi)發(fā)一個(gè)高性能的Web應(yīng)用程序的關(guān)鍵不會(huì)在語(yǔ)言或語(yǔ)言運(yùn)行環(huán)境上,.NET、RoR、PHP、Java等等在這一領(lǐng)域都表現(xiàn)良好。
相關(guān)文章
利用C#實(shí)現(xiàn)獲取當(dāng)前設(shè)備硬件信息
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)獲取當(dāng)前設(shè)備硬件信息的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-03-03解析C#設(shè)計(jì)模式編程中備忘錄模式的運(yùn)用
這篇文章主要介紹了C#設(shè)計(jì)模式編程中備忘錄模式的運(yùn)用,備忘錄模式用來(lái)保存與對(duì)象有關(guān)的數(shù)據(jù)用以在將來(lái)對(duì)對(duì)象進(jìn)行復(fù)原,需要的朋友可以參考下2016-02-02C#/VB.NET 實(shí)現(xiàn)在PDF表格中添加條形碼
條碼的應(yīng)用已深入生活和工作的方方面面。在處理?xiàng)l碼時(shí),常需要和各種文檔格式相結(jié)合。本文,以操作PDF文件為例,介紹如何在編輯表格時(shí),向單元格中插入條形碼,需要的可以參考一下2022-06-06C# WPF實(shí)現(xiàn)動(dòng)態(tài)3D光照效果
這篇文章主要為大家詳細(xì)介紹了如何利用C# WPF實(shí)現(xiàn)動(dòng)態(tài)3D的光照效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03C#實(shí)現(xiàn)獲取文件大小并進(jìn)行比較
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)獲取文件大小進(jìn)行單位轉(zhuǎn)換與文件大小比較功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-03-03C# OpenCvSharp實(shí)現(xiàn)圖片批量改名
這篇文章主要為大家詳細(xì)介紹了C#如何結(jié)合OpenCvSharp實(shí)現(xiàn)圖片批量改名功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03