zoukankan      html  css  js  c++  java
  • 反射性能优化2

    反射性能优化2

     在上篇博客中,我介绍了优化反射的第一个步骤:用委托调用代替直接反射调用。
      然而,那只是反射优化过程的开始,因为新的问题出现了:如何保存大量的委托?

      如果我们将委托保存在字典集合中,会发现这种设计会浪费较多的执行时间,因为这种设计会引发三个新问题:
      1. 代码的执行路径变长了。
      2. 字典查找是有成本开销的。
      3. 字典集合的并发读写需要锁定,会影响并发性。

      再来回顾一下上次的测试结果吧:

      虽然通用接口ISetValue将反射性能优化了37倍,但是最终的FastSetValue将这个数字减少到还不到7倍(在CLR4中还不到5倍)。
      难道您不觉得遗憾吗?

      再看看直接调用与反射调用的对比,它们的速度相差了上千倍!

      能不能不使用委托?

      既然委托最后引出了三个难以解决的问题,导致优化后速度比直接调用差距太远,那我们能不能不使用委托呢?

      委托调用并不是优化反射的唯一方案,我们还有其它方法,
      之所以委托调用能成为常见的优化方案是因为它比较简单。

      假如我需要用客户端提交的数据来填充某个数据对象,考虑到代码的通用性,我会用反射写成这样:

    /// <summary>
    /// 从HttpRequest加载obj所需的数据
    /// </summary>
    /// <param name="request"></param>
    /// <param name="obj"></param>
    public static void LoadDataFromHttpRequest(HttpRequest request, object obj)
    {
        PropertyInfo[] properties = obj.GetType().GetProperties();
        foreach( PropertyInfo p in properties ) {
            // 这里只是示意代码,假设数据处理不会有异常。
            object val = Convert.ChangeType(request[p.Name], p.PropertyType);
            p.FastSetValue(obj, val);
        }
    }
    

      如果我事先知道要加载已知的数据类型,代码会写成这样:

    public static void LoadDataFromHttpRequest(HttpRequest request, OrderInfo order)
    {
        // 这里只是示意代码,假设数据处理不会有异常。
        order.OrderID = int.Parse(request["OrderID"]);
        order.OrderDate = DateTime.Parse(request["OrderDate"]);
        order.SumMoney = decimal.Parse(request["SumMoney"]);
        order.Comment = request["Comment"];
        order.Finished = bool.Parse(request["Finished"]);
    }
    

      显然,第二段代码运行效率更快(尽管第一段代码调用FastSetValue优化了速度)。

      大家都知道反射性能较差,直接调用性能最好,那么能不能在运行时不使用反射呢?

      的确,使用反射是因为我们事先不知道要处理哪些类型的对象,因此不得不用反射, 另外,反射的代码也更通用,写一个方法可以加载所有的数据类型,可认为是一劳永逸的方法。 不过,就算我们事先不知道要处理哪些对象类型,但是只要使用反射,我们完全可以知道任何一个类型包含哪些数据成员, 还能知道这些数据成员的数据类型,这一点不用怀疑吧? 既然我们用反射可以知道所有的类型定义信息,我们是否可以参照代码生成器的思路去生成代码呢? 我们可以参照前面第二段代码,为【需要处理的类型】生成直接调用的代码,这样不就彻底解决了反射性能问题了吗? 生成代码的过程,其实也就是个字符串的拼接过程,难度并不大,只是比较复杂而已。

      如果前面的答案都是肯定的,那么现在只有一个问题了:我们能在运行时执行拼接生成的字符串代码吗?

      答案也是肯定的:能!

      CodeDOM:在运行时编译代码

      回忆一下我们编写的ASPX页面,它们并不是C#代码,它们本质上就是一个文本文件, 我们可以写入一些HTML标签,还有些标签上加了 runat="server" 属性, 我们还可以在页面中插入一些C#代码片段,尽管它们不是我们编译后的DLL文件,然而它们就是运行起来了! 要知道ASP.NET不是ASP,ASP是解释性的脚本语言,而ASP.NET是以编译方式运行的, 所以,每个ASPX页面文件最后都是运行编译后的结果。

      假设我有下面一段文本(文本的内容是一段C#代码):

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Reflection;
    
    namespace OptimizeReflection
    {
        public class DemoClass
        {
            public int Id { get; set; }
    
            public string Name;
    
            public int Add(int a, int b)
            {
                return a + b;
            }
        }
    
        public class 用户手册
        {
            public static void Main()
            {
                // OptimizeReflection 这个类库提供了一些扩展方法,它们用于优化常见的反射场景
                // 下面是一些相关的演示示例。
                
                // 对于属性的读写操作、方法的调用操作,还提供了性能更好的强类型(泛型)版本,可参考Program.cs
    
                Type instanceType = typeof(DemoClass);
                PropertyInfo propertyInfo = instanceType.GetProperty("Id");
                FieldInfo fieldInfo = instanceType.GetField("Name");
                MethodInfo methodInfo = instanceType.GetMethod("Add");
    
                // 1. 创建实例对象
                DemoClass obj = (DemoClass)instanceType.FastNew();
    
                // 2. 写属性
                propertyInfo.FastSetValue(obj, 123);
                propertyInfo.FastSetValue2(obj, 123);
    
                // 3. 读属性
                int a = (int)propertyInfo.FastGetValue(obj);
                int b = (int)propertyInfo.FastGetValue2(obj);
    
                // 4. 写字段
                fieldInfo.FastSetField(obj, "Fish Li");
    
                // 5. 读字段
                string s = (string)fieldInfo.FastGetValue(obj);
    
                // 6. 调用方法
                int c = (int)methodInfo.FastInvoke(obj, 1, 2);
                int d = (int)methodInfo.FastInvoke2(obj, 3, 4);
    
                Console.WriteLine("a={0}; b={1}; c={2}; d={3}; s={4}", a, b, c, d, s);
            }
        }
    }
    
    

      您可以把上面这段文本想像成前面第二个版本的LoadDataFromHttpRequest方法,如果我们在运行时使用反射也能生成那样的代码, 现在就差把它编译成程序集了。下面的代码演示了如何将一段文本编译成程序集的过程:

    string code = null;
    
    // 1. 生成要编译的代码。(示例为了简单直接从程序集内的资源中读取)
    Stream stram = typeof(CodeDOM).Assembly
                .GetManifestResourceStream("TestOptimizeReflection.用户手册.txt");
    using( StreamReader sr = new StreamReader(stram) ) {
        code = sr.ReadToEnd();
    }
    
    //Console.WriteLine(code);
    
    // 2. 设置编译参数,主要是指定将要引用哪些程序集
    CompilerParameters cp = new CompilerParameters();
    cp.GenerateExecutable = false;
    cp.GenerateInMemory = true;
    cp.ReferencedAssemblies.Add("System.dll");
    cp.ReferencedAssemblies.Add("OptimizeReflection.dll");
    
    // 3. 获取编译器并编译代码
    // 由于我的代码使用了【自动属性】特性,所以需要 C# .3.5版本的编译器。
    // 获取与CLR匹配版本的C#编译器可以这样写:CodeDomProvider.CreateProvider("CSharp")
    
    Dictionary<string, string> dict = new Dictionary<string, string>();
    dict["CompilerVersion"] = "v3.5";
    dict["WarnAsError"] = "false";
    
    CSharpCodeProvider csProvider = new CSharpCodeProvider(dict);
    CompilerResults cr = csProvider.CompileAssemblyFromSource(cp, code);
    
    // 4. 检查有没有编译错误
    if( cr.Errors != null && cr.Errors.HasErrors ) {
        foreach( CompilerError error in cr.Errors )
            Console.WriteLine(error.ErrorText);
    
        return;
    }
    
    // 5. 获取编译结果,它是编译后的程序集
    Assembly asm = cr.CompiledAssembly;
    
    

      整个过程分为5个步骤,它们已用注释标识出来了,这里不再重复了。

      如何调用编译结果

      前面的代码把一段文本字符串编译成了程序集,现在还有最后一个问题:如何调用编译结果?

      答案:有二种方法,
      1. 直接调用方法。
      2. 实例化程序集中的类型,以接口方式调用方法。
      其实这二种方法都需要使用反射,用反射定位到要调用的类型和方法。

      第一种方法要求在生成代码时,生成的类名和方法名是明确的,在调用方法时,我们有二个选择:
      1. 用反射的方式调用(这里只是一次反射)。
      2. 为方法生成委托(用上篇博客介绍的方法),然后基于委托调用。

      第二种方法要求在生成代码时,首先要定义一个接口,保证生成的代码能实现指定的接口,
      然而用反射找到要调用的类型名称,用反射或者委托调用构造方法创建类型实例,最后基于接口去调用。
      我们熟悉的ASPX页面就是采用了这种方式来实现的。

      这二种方法也可以这样区分:
      1. 如果生成的方法是静态方法,应该选择第一种方法。
      2. 如果生成的方法是实例方法,那么选择第二种方法是合理的。

      对于前面的示例,我采用了第一种方法了,因为类名和方法名称都是事先确定的而且实现起来比较简单。

    // 6. 找到目标方法,并调用
    Type t = asm.GetType("OptimizeReflection.用户手册");
    MethodInfo method = t.GetMethod("Main");
    method.Invoke(null, null);

      能不能不使用委托? 如何用好CodeDOM?
      在这篇博客中我不知道把它们安排在哪里较为合适,算了,还是把答案留给下篇博客吧。

  • 相关阅读:
    程序员:不要自称为码农
    SpringBoot对静态资源配置
    LeetCode 572. Subtree of Another Tree(子树)
    LeetCode 437. Path Sum III(统计路径和等于sum的路径数量)
    LeetCode 112. Path Sum(判断路径和是否等于一个数)
    LeetCode 617. Merge Two Binary Trees(归并两棵二叉树)
    LeetCode 226. Invert Binary Tree(翻转二叉树)
    Failure to transfer org.apache.maven.plugins:maven-resources-plugin:pom:2.6 的解决办法
    linux-查询某软件的安装的目录
    WebService概念解释
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/10458268.html
Copyright © 2011-2022 走看看