使用动态生成的委托提高调用动态程序集的性能
前言
在一些时候,我们需要动态生成一个函数,例如最近银河发表的一篇随笔当中提及到的《画函数图形的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 支持:
然后就是其他测试了。这里就不贴出来了,代码等下提供下载。
测试结果
我分别在 Debug 和 Release 模式中测试了一下,发现相对差别不大,都是首次调用慢一点,以后的调用就快一点。左图为 Debug 模式,右图为 Release 模式,循环 65536 次。(点击放大)
最后
这个貌似比老赵提供的数据快,但我相信原理是一样的。还有怎么在这个场景使用老赵的 Fast Reflection Library 和他推荐的 Dynamic Reflection Library 呢?欢迎大家指教。
[EDIT]
睡了一觉,想一想老赵的回复是对的,应用的场景是不同的,那么 Dynamic Reflection Library 呢?继续欢迎大家讨论,赐教!
http://DiryBoy.cnblogs.com