欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-03 18:53:21
想调用一个方法很容易,直接代码调用就行,这人人都会。其次呢,还可以使用反射。不过通过反射调用的性能会远远低于直接调用——至少从绝对时间上来看的确是这样。虽然这是个众所周知的...

想调用一个方法很容易,直接代码调用就行,这人人都会。其次呢,还可以使用反射。不过通过反射调用的性能会远远低于直接调用——至少从绝对时间上来看的确是这样。虽然这是个众所周知的现象,我们还是来写个程序来验证一下。比如我们现在新建一个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等等在这一领域都表现良好。