使用动态生成的委托提高调用动态程序集的性能

前言

在一些时候,我们需要动态生成一个函数,例如最近银河发表的一篇随笔当中提及到的《画函数图形的C#程序(改进版)》。不久之前,我们伟大的老赵也发过一篇《方法的直接调用,反射调用与……Lambda表达式调用》,他也推荐了《Dynamic Reflection Library》,但我不懂怎么应用在这里,因为这里的输入是字符串。自己用 CodeDom 实现了一下(其实这个我在没有来博客园之前就想实现了,当时刚学 .Net 不久,走了很多弯路才勉强可行,博客园真是个学习和提高的好地方^_^),发现性能提高了非常多,故在这里跟大家分享一下。

 

思想和目标

首先,普遍知识,MethodInfo.Invoke 需要查找元数据,很慢,然而通过一个委托调用某方法跟调用这个方法的速度差不多。因此,在需要经常重复调用一个方法的时候,不应该使用 MethodInfo.Invoke 这个方法。

然而,偏偏这个方法是由用户动态输入的,那么,解决的思路是把用户输入编译成动态程序集中的一个方法,却不直接 MethodInfo.Invoke 它,而是在这个动态程序集中另外编写一个方法,返回一个委托,指向目标方法,然后 MethodInfo.Invoke 这个另外编写的方法,获得一个委托,之后就可以重复调用我们动态生成的目标方法却不用 MethodInfo.Invoke 了,而且是 TypeSafe 的,还有 IDE 的智能提示支持,多好!

在没有 Lambda 表达式甚至没有匿名方法的时代(就是我刚有这个想法的时代),要实现这个很麻烦,这是因为委托的类型问题。(见过“XYZ 类型不能强制转换为 XYZ 类型”这种错误吗? XYZ = XYZ)

自从有了 Linq 以后,BCL 中多了一系列形如 Func<T, TResult> 这样的委托类型,这种思想的实现就更容易了,Lambda 表达式还能免去我们多写一个方法的痛苦,实在太好了。

 

那还等什么?编程实现!

首先,编写核心的动态程序集代码以及编译。

private static Assembly getDynamicAssembly()
{
    string source = @"
    public class DynamicClass
    {
        public static System.Func<double, double> GetFunc()
        {
            return ( double x ) =>
            {
                return x + x;
            };
        }

        public static double Add( double x )
        {
            return x + x;
        }
    }";
    var providerOptions = new Dictionary<string, string> { { "CompilerVersion", "v3.5" } };
    var cp = new CSharpCodeProvider(providerOptions);

    CompilerParameters cps = new CompilerParameters();
    cps.GenerateExecutable = false;
    cps.GenerateInMemory = true;
    cps.IncludeDebugInformation = false;
    cps.CompilerOptions += "/optimize /reference:System.Core.dll";

    return cp.CompileAssemblyFromSource(cps, source).CompiledAssembly;
}

说明一下,private static 是因为我还没有封装,只是写了一个 ConsoleApp 来证明想法。OK,source 里的明星方法是 GetFunc(),它返回一个 Func<double, double> 委托,委托指向的方法由 Lambda 表达式构造,这个 Lambda 表达式的内容就是我们要动态生成的内容。另外,在使用 CSharpCodeProvider 的时候默认是用 2.0 的,所以需要配置一下,具体可以查阅 MSDN Library,这里就直接 hard code 进去了。实际应用的时候,动态编译可能会出错,这里也忽略了。Add 方法是等一下要 MethodInfo.Invoke 的对比方法。

接着,就是编写个方法测试一下效果,很简单:

private static Stopwatch measureDynamicDelegate( Assembly asm, int length )
{
    var func = asm.GetType("DynamicClass").GetMethod("GetFunc").Invoke(null, null) as Func<double, double>;
    var sw = Stopwatch.StartNew();
    for ( int i = 0; i < length; i++ )
    {
        func(1d);
    }
    sw.Stop();
    return sw;
}

可以看到,要使用很简单,只需写 func(1d),而且还有 IDE 的 Intellisense 支持:

Intellisense Support

然后就是其他测试了。这里就不贴出来了,代码等下提供下载。

 

测试结果

我分别在 Debug 和 Release 模式中测试了一下,发现相对差别不大,都是首次调用慢一点,以后的调用就快一点。左图为 Debug 模式,右图为 Release 模式,循环 65536 次。(点击放大)

Debug Release

 

测试代码下载

 

最后

这个貌似比老赵提供的数据快,但我相信原理是一样的。还有怎么在这个场景使用老赵的 Fast Reflection Library 和他推荐的 Dynamic Reflection Library 呢?欢迎大家指教。

[EDIT]

睡了一觉,想一想老赵的回复是对的,应用的场景是不同的,那么 Dynamic Reflection Library 呢?继续欢迎大家讨论,赐教!

posted @ 2009-05-04 17:33  DiryBoy  阅读(4231)  评论(18编辑  收藏  举报