C#中方法的直接调用、反射调用与Lambda表达式调用对比

 更新时间:2020年6月25日 11:29  点击:2108

想调用一个方法很容易,直接代码调用就行,这人人都会。其次呢,还可以使用反射。不过通过反射调用的性能会远远低于直接调用——至少从绝对时间上来看的确是这样。虽然这是个众所周知的现象,我们还是来写个程序来验证一下。比如我们现在新建一个Console应用程序,编写一个最简单的Call方法。

复制代码 代码如下:

class Program
{
    static void Main(string[] args)
    {
       
    }

    public void Call(object o1, object o2, object o3) { }
}


Call方法接受三个object参数却没有任何实现,这样我们就可以让测试专注于方法调用,而并非方法实现本身。于是我们开始编写测试代码,比较一下方法的直接调用与反射调用的性能差距:
复制代码 代码如下:

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();
}


执行结果如下:
复制代码 代码如下:

00:00:00.0119041 (Directly invoke)
00:00:04.5527141 (Reflection invoke)
Press any key to continue...

通过各调用一百万次所花时间来看,两者在性能上具有数量级的差距。因此,很多框架在必须利用到反射的场景中,都会设法使用一些较高级的替代方案来改善性能。例如,使用CodeDom生成代码并动态编译,或者使用Emit来直接编写IL。不过自从.NET 3.5发布了Expression相关的新特性,我们在以上的情况下又有了更方便并直观的解决方案。

了解Expression相关特性的朋友可能知道,System.Linq.Expressions.Expression<TDelegate>类型的对象在调用了它了Compile方法之后将得到一个TDelegate类型的委托对象,而调用一个委托对象与直接调用一个方法的性能开销相差无几。那么对于上面的情况,我们又该得到什么样的Delegate对象呢?为了使解决方案足够通用,我们必须将各种签名的方法统一至同样的委托类型中,如下:

复制代码 代码如下:

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;
    };
}


如上,我们就得到了一个Func<object, object[], object>类型的委托,这意味它接受一个object类型与object[]类型的参数,以及返回一个object类型的结果——等等,朋友们有没有发现,这个签名与MethodInfo类型的Invoke方法完全一致?不过可喜可贺的是,我们现在调用这个委托的性能远高于通过反射来调用了。那么对于有返回值的方法呢?那构造一个委托对象就更方便了:
复制代码 代码如下:

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();
}


至此,我想朋友们也已经能够轻松得出调用静态方法的委托构造方式了。可见,这个解决方案的关键在于构造一个合适的Expression<TDelegate>,那么我们现在就来编写一个DynamicExecuter类来作为一个较为完整的解决方案:

复制代码 代码如下:

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的关键就在于GetExecuteDelegate方法中构造Expression Tree的逻辑。如果您对于一个Expression Tree的结构不太了解的话,不妨尝试一下使用Expression Tree Visualizer 来对一个现成的Expression Tree进行观察和分析。我们将一个MethodInfo对象传入DynamicMethodExecutor的构造函数之后,就能将各组不同的实例对象和参数对象数组传入Execute进行执行。这一切就像使用反射来进行调用一般,不过它的性能就有了明显的提高。例如我们添加更多的测试代码:

复制代码 代码如下:

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)");

现在的执行结果则是:

复制代码 代码如下:

00:00:00.0125539 (Directly invoke)
00:00:04.5349626 (Reflection invoke)
00:00:00.0322555 (Dynamic executor)
Press any key to continue...

事实上,Expression<TDelegate>类型的Compile方法正是使用Emit来生成委托对象。不过现在我们已经无需将目光放在更低端的IL上,只要使用高端的API来进行Expression Tree的构造,这无疑是一种进步。不过这种方法也有一定局限性,例如我们只能对公有方法进行调用,并且包含out/ref参数的方法,或者除了方法外的其他类型成员,我们就无法如上例般惬意地编写代码了。

补充

木野狐兄在评论中引用了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等等在这一领域都表现良好。

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • php 中file_get_contents超时问题的解决方法

    file_get_contents超时我知道最多的原因就是你机器访问远程机器过慢,导致php脚本超时了,但也有其它很多原因,下面我来总结file_get_contents超时问题的解决方法总结。...2016-11-25
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • php抓取网站图片并保存的实现方法

    php如何实现抓取网页图片,相较于手动的粘贴复制,使用小程序要方便快捷多了,喜欢编程的人总会喜欢制作一些简单有用的小软件,最近就参考了网上一个php抓取图片代码,封装了一个php远程抓取图片的类,测试了一下,效果还不错分享...2015-10-30
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • HTTP 408错误是什么 HTTP 408错误解决方法

    相信很多站长都遇到过这样一个问题,访问页面时出现408错误,下面一聚教程网将为大家介绍408错误出现的原因以及408错误的解决办法。 HTTP 408错误出现原因: HTT...2017-01-22
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25