Spiga

Fast Reflection Library

2009-02-01 09:25 by Jeffrey Zhao, 11708 visits, 收藏, 编辑

全文英文版:Fast Reflection Library

这是我在CodePlex上创建的一个项目,它的网址是http://www.codeplex.com/FastReflectionLib,使用Microsoft Public License (Ms-PL),您可以随意在自己的产品中使用它的全部或部分代码。这个项目用到了我在《方法的直接调用,反射调用与Lambda表达式调用》和《这下没理由嫌Eval的性能差了吧?》两篇文章里用到的做法,并加以提炼和扩展发布的项目——随便搞搞,留个印记,也供以后参考。

基本使用方式

反射是.NET中非常重要的功能。使用反射来构造对象、调用方法或是访问属性是某些项目中常用的做法之一(例如ORM框架)。众所周知,与一个成员的直接访问相比,反射调用的性能要低好几个数量级。FastReflectionLib提供了一种简便的方式,使一些常用反射调用的性能得到大幅提高。如下:

using System;
using System.Reflection;
using FastReflectionLib;

namespace SimpleConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            PropertyInfo propertyInfo = typeof(string).GetProperty("Length");
            MethodInfo methodInfo = typeof(string).GetMethod("Contains");

            string s = "Hello World!";

            // get value by normal reflection
            int length1 = (int)propertyInfo.GetValue(s, null);
            // get value by the extension method from FastReflectionLib,
            // which is much faster
            int length2 = (int)propertyInfo.FastGetValue(s);

            // invoke by normal reflection
            bool result1 = (bool)methodInfo.Invoke(s, new object[] { "Hello" });
            // invoke by the extension method from FastReflectionLib,
            // which is much faster
            bool result2 = (bool)methodInfo.FastInvoke(s, new object[] { "Hello" });
        }
    }
}

在得到了PropertyInfo或MethodInfo对象之后,我们可以使用GetValue或Invoke方法来访问属性或调用方法。在FastReflectionLib中为PropertyInfo、MethodInfo等对象定义了对应的扩展方法,于是我们就可以使用这些扩展方法(从代码上看来,基本上只是在原来的方法之前加上“Fast”)来进行调用,与之前的方法相比,新的扩展方法性能有极大的提高。

直接使用各工作对象

各FastXxx方法实际上是将PropertyInfo等对象作为Key去一个Cache中获取对应的工作对象,然后调用工作对象上对应的方法。因此,直接调用工作对象可以获得更好的性能。各工作对象类型的对应关系如下:

  • PropertyInfo:IPropertyAccessor
  • MethodInfo:IMethodInvoker
  • ConstructorInfo:IConstructorInvoker
  • FieldInfo:IFieldAccessor

我们可以使用FastReflectionCaches.MethodInvokerCache来获取一个IMethodInvoker对象:

static void Execute(MethodInfo methodInfo, object instance, int times)
{ 
    IMethodInvoker invoker = FastReflectionCaches.MethodInvokerCache.Get(methodInfo);
    object[] parameters = new object[0];
    for (int i = 0; i < times; i++)
    {
        invoker.Invoke(instance, parameters);
    }
}

工作对象的默认实现与扩展

在FastReflectionLib中,已经提供了IPropertyAccessor等接口的默认实现。该实现将会构造一颗表达式树(Expression Tree)并将其编译(调用其Compile方法)以获得一个与反射方法签名相同的委托对象。这是一种简单、通用而安全的实现,由于Compile方法使用了Emit,其性能也较为令人满意(可见下面的性能测试)。但是这并不是性能最高的做法,如果使用Emit生成最优化的代码,其性能甚至会高于方法的直接调用(例如Dynamic Reflection Library)。如果您想使用更好的实现来替换,则可以自行构造一个工作对象接口的实现,并替换对应的Factory:

public class BetterPropertyAccessor : IPropertyAccessor
{
    public BetterPropertyAccessor(PropertyInfo propertyInfo) { ... }

    ...
}

public class BetterPropertyAccessorFactory :
    IFastReflectionFactory<PropertyInfo, IPropertyAccessor>
{
    public IPropertyAccessor Create(PropertyInfo key)
    {
        return new BetterPropertyAccessor(key);
    }
}

class Program
{
    static void Main(string[] args)
    {
        FastReflectionFactories.PropertyAccessorFactory =
            new BetterPropertyAccessorFactory();

        ...
    }
}

缓存的默认实现与扩展

在FastReflectionLib中使用基于System.Collections.Generic.Dictionary<TKey, TValue>类型编写的缓存容器。每次调用FastXxx扩展方法时,类库将从对应的缓存容器中获取工作对象。如果缓存容器中还没有所需的工作对象,那么它就会调用合适的Factory来构造新的工作对象。从下面的性能测试来看,许多时间是消耗在缓存查找上的,如果您有更好的缓存实现,可以使用以下的方法替换默认的缓存的容器:

public class BetterMethodInvokerCache :
    IFastReflectionCache<MethodInfo, IMethodInvoker>
{
    public IMethodInvoker Get(MethodInfo key) { ... }
}

class Program
{
    static void Main(string[] args)
    {
        FastReflectionCaches.MethodInvokerCache = 
            new BetterMethodInvokerCache();

        ...
    }
}

根据需要自行缓存工作对象

FastReflectionLib中通过PropertyInfo等对象作为Key,对PropertyAccessor等工作对象进行缓存。但是在某些场景下,您也可以选择合适的方式来自行缓存工作对象。与FastReflectionLib源码同时发布的CustomCache示例网站中包含了一个FastEval扩展,在某些场景下,我们可以使用这个更高效的方法来替换内置的Eval方法。这个示例的特点如下:

  • 使用对象的类型和属性名同时作为缓存的Key获取对应的PropertyAccessor对象
  • 使用PropertyAccessor获取“匿名对象”中的属性值
  • 缓存的作用域为特定页面,而不是整个AppDomain。

性能测试

FastReflectionLib源码中包含了一个性能测试项目,您可以从中看出FastReflectionLib对于反射的性能改进。摘录部分数据如下(测试在我的笔记本上运行,Release编译)。

执行以下方法:

public class Test
{
    public void MethodWithArgs(int a1, string a2) { }
}

进行一百万次调用,结果如下:

调用方式 消耗时间(秒)
方法直接调用 0.0071397
内置反射调用 1.4936181
工作对象调用 0.0468326
Fast方法调用 0.1373712
Add your comment

55 条回复

  1. #1楼 萧寒      2009-01-31 22:50
    好东西,收藏
     回复 引用 查看   
  2. #2楼 天生俪姿      2009-01-31 22:51
    哈哈不知道是不是沙发呢。
    这般文章好像是你英文文章的翻译版本是吧。我关注了那篇英文文章
    看到了一半就不行了。不懂的单词太多。主要是对于这些技术上的东西懂的太少
    关注一下。看看中文的先
     回复 引用 查看   
  3. #3楼 天生俪姿      2009-01-31 22:51
    哎~!一个板凳
    老赵好像天天晚上在线
     回复 引用 查看   
  4. #4楼 gyf19[未注册用户]2009-01-31 22:53
    不错,.net2.0 可以使用吗
     回复 引用   
  5. #5楼[楼主] Jeffrey Zhao      2009-01-31 22:55
    --引用--------------------------------------------------
    天生俪姿: 哈哈不知道是不是沙发呢。
    这般文章好像是你英文文章的翻译版本是吧。我关注了那篇英文文章
    看到了一半就不行了。不懂的单词太多。主要是对于这些技术上的东西懂的太少
    关注一下。看看中文的先
    --------------------------------------------------------
    忘了放我的英文链接了……今天懒得改了,明天放。
     回复 引用 查看   
  6. #6楼[楼主] Jeffrey Zhao      2009-01-31 22:56
    --引用--------------------------------------------------
    天生俪姿: 哎~!一个板凳
    老赵好像天天晚上在线
    --------------------------------------------------------
    对于stateless的系统来说,老赵一直在线的,嘿嘿
     回复 引用 查看   
  7. #7楼[楼主] Jeffrey Zhao      2009-01-31 22:56
    --引用--------------------------------------------------
    gyf19: 不错,.net2.0 可以使用吗
    --------------------------------------------------------
    不可以
     回复 引用 查看   
  8. #8楼 天生俪姿      2009-01-31 22:56
    问下老赵这个东西是你自己搞的吗?
    源自哪里?我说不是指你在Code Plex 做的什么。
     回复 引用 查看   
  9. #9楼 JimLiu      2009-01-31 22:57
    很棒,呀哈!
     回复 引用 查看   
  10. #10楼 天生俪姿      2009-01-31 23:01
    stateless
    这个词原来是 无国藉的意思。
    哎~!还好去了谷歌查了一下
    这个是3.5 的产物是吧。
    能下载的东东是全部的代码吗?
     回复 引用 查看   
  11. #11楼[楼主] Jeffrey Zhao      2009-01-31 23:03
    --引用--------------------------------------------------
    天生俪姿: 问下老赵这个东西是你自己搞的吗?
    源自哪里?我说不是指你在Code Plex 做的什么。
    --------------------------------------------------------
    自己搞得,源自.NET 3.5中Lamda Expression的Compile功能,呵呵。
     回复 引用 查看   
  12. #12楼[楼主] Jeffrey Zhao      2009-01-31 23:04
    --引用--------------------------------------------------
    天生俪姿: stateless
    这个词原来是 无国藉的意思。
    哎~!还好去了谷歌查了一下
    这个是3.5 的产物是吧。
    能下载的东东是全部的代码吗?
    --------------------------------------------------------
    嗯,全部代码,包括性能测试和一个示例。
     回复 引用 查看   
  13. #13楼 重典      2009-01-31 23:23
    downloading
    支持一下

    看了看测试感觉效果不错
     回复 引用 查看   
  14. #14楼[楼主] Jeffrey Zhao      2009-02-01 00:04
    @重典
    恩,够用了
     回复 引用 查看   
  15. #15楼 virusplayer[未注册用户]2009-02-01 00:40
    老赵的东西
    支持一下
     回复 引用   
  16. #16楼 张柔      2009-02-01 00:42
    听上去很不错的样子

    我想问一个问题

    我项目里三层的数据层用到了反射工厂
    我感觉查询一多的话会频繁调用反射工厂

    有必要做优化吗
     回复 引用 查看   
  17. #17楼[楼主] Jeffrey Zhao      2009-02-01 00:45
    @张柔
    任何性能上的猜测都要通过profiler来检验才能说明问题。
     回复 引用 查看   
  18. #18楼[楼主] Jeffrey Zhao      2009-02-01 00:45
    --引用--------------------------------------------------
    virusplayer: 老赵的东西
    支持一下
    --------------------------------------------------------
    恩,谢谢
     回复 引用 查看   
  19. #19楼 张柔      2009-02-01 00:50
    @Jeffrey Zhao
    这个东西都没有涉及过

    是不是IDE里的分析-启动性能向导
     回复 引用 查看   
  20. #20楼[楼主] Jeffrey Zhao      2009-02-01 01:00
    @张柔
    都可以,自己插入计算代码也可以。
     回复 引用 查看   
  21. #21楼 梁逸晨      2009-02-01 02:55
    老赵,问个和主题不相关的:

    1 codeplex支不支持SVN?或者类似的VSS那些都行,只要能备份代码就行了。
    2 如果能够备份,有没有那种不允许公共查看源码的功能?


    烧了几块硬盘了,再也不敢相信本地主机的安全性。目前在用googlecode,但是强制面向公共开源,有点项目又不太合适这么做。
     回复 引用 查看   
  22. #22楼 Anders Liu      2009-02-01 07:37
    @梁逸晨

    一看代码的目录结构就知道老赵用的是svn bridge。强人,我一直用TFC的……

    开源,就是开放远爱吗,不允许公共查看,还叫啥开源啊。

    老赵一看也是设计模式控,回头仔细研究一下。
     回复 引用 查看   
  23. #23楼 kkun      2009-02-01 09:21
    标记!学习!谢谢!
     回复 引用 查看   
  24. #24楼[楼主] Jeffrey Zhao      2009-02-01 09:28
    @梁逸晨
    svn可以的,不能不开放——当然可以让你存30天
     回复 引用 查看   
  25. #25楼[楼主] Jeffrey Zhao      2009-02-01 09:29
    @Anders Liu
    习惯svn了阿,client也是现成的,不会用tfc了……
    // 这嘎达没有啥设计模式的
     回复 引用 查看   
  26. #26楼 Tony Qu      2009-02-01 10:00
    不错,不过不知道封装之后性能到底如何,有待考证:p
     回复 引用 查看   
  27. #27楼[楼主] Jeffrey Zhao      2009-02-01 10:09
    @Tony Qu
    什么东西封装之后?
     回复 引用 查看   
  28. #28楼 Zealic      2009-02-01 10:28
    除了直接反射(性能最差的) 和直接调用
    其他技术的实现都是基于 DynamicMethod 实现。
    包括所谓的 Dynamic Reflection Library。
    其实这样的工作已经有很多的实现。

    比如
    NHibernate 的 NHibernate.Property
    IBatis.Net 的 IBatisNet.Common.Utilities.Objects.Members
    似乎 Spring.Net 中也有,这点就懒得验证了。

    当然,知道这么多是因为我自己也写过这样的东西。
    完善以后就发出来。
     回复 引用 查看   
  29. #29楼[楼主] Jeffrey Zhao      2009-02-01 10:59
    @Zealic
    不错——幸好这个Lambda做法没人搞过。
     回复 引用 查看   
  30. #30楼 真见      2009-02-01 12:10
    我看了,在weblogs上也写过了。。
     回复 引用 查看   
  31. #31楼 --[未注册用户]2009-02-01 17:13
    我想知道如何申请Weblogs的地址?
     回复 引用   
  32. #32楼 Kolor      2009-02-02 10:58
    其实,这个类库在反射的性能提升上,主要就是使用了cache,适用于同一(相同引用,因为cache的存储结构是以对象为Key的字典)PropertyInfo或者MethodInfo等的多次(比如object类型属性的Get操作,调用次数在600次以上才有性能提升)反射调用,在这种情况下,性能得以提升,而且随着调用次数的增加,性能会得到更大的提升;但是如果只是一次调用的话,结果只会适得其反,比起正常的反射调用,性能要慢大概两个数量级。 所以说,少量调用还是不建议使用这个类库,不知道我说的对不对 另外,我对Lambda不是很熟,楼主能说下在这个类库中使用Lambda的好处吗?对比其他方法,它有什么优势呢?
     回复 引用 查看   
  33. #33楼[楼主] Jeffrey Zhao      2009-02-02 11:28
    @Kolor
    嗯?不是阿,是方法调用的开销上的提高,不是cache获取和GetMethod方法性能上的差距。
    用Lambda表达式的好处就是方便,安全,清晰。
     回复 引用 查看   
  34. #34楼 Kolor      2009-02-02 11:52
    @Jeffrey Zhao
    你的设计应该是,第一次调用取得工作方法,并把它放入cache,下次直接从cache取,所以性能上有提升,是这样的吧? 你调低循环次数times,再看看测试结果
     回复 引用 查看   
  35. #35楼[楼主] Jeffrey Zhao      2009-02-02 12:24
    @Kolor
    构造一个MethodInvoker自然比直接调用一次MethodInfo要慢。
    你可以比较一下“内置反射调用”与“工作对象调用”性能的差距,这才是最大的优化。
     回复 引用 查看   
  36. #36楼 Chester      2009-02-03 01:48
    Very good. But I think you could get rid of factory layer and name cache as factory.
     回复 引用 查看   
  37. #37楼[楼主] Jeffrey Zhao      2009-02-03 09:48
    @Chester
    为什么,完全不一样的东西,现在可以单独替换cache和factory实现,两个东西完全是正交的。
     回复 引用 查看   
  38. #38楼 Chester      2009-02-04 01:50
    It is only my preference as I don't see much value for one extra layer and the cache is only a performance sweetener. I assume no one would replace it. Instead I would like to see a simpler design. That's it. Thanks anyway. One more thing is parameter type conversion. For example, if I have a method: void Method(double d) While calling it like this: Method(1). The generated method will throw InvalidCastException. I used to use Convert.ChangeType to convert the type of parameter value while in reflection calls.
     回复 引用 查看   
  39. #39楼[楼主] Jeffrey Zhao      2009-02-04 09:03
    @Chester
    嗯,多谢提醒,我看看是不是要用Convert.ChangeType去做。
    不过cache,我觉得现在cache消耗还是蛮大的,以后应该有方法,或者有需要去替换它。
     回复 引用 查看   
  40. #40楼 大 兵      2009-02-05 09:12
    好东西,拿回去研究下。
     回复 引用 查看   
  41. #41楼 guozili[未注册用户]2009-02-12 20:08
    看了看代码,和你想法差不多,有几点疑问:

    1. 动态构建lambda expression性能消耗好像是在lambda.Compile(),所以把每个Compile后的Func缓存起来。

    2. 动态创建lambda expression是否在运行期也会编译成中间代码,最终和手写Emit本质一样,只不过构造expression代码清晰安全易懂。
     回复 引用   
  42. #42楼 guozili[未注册用户]2009-02-12 20:12
    3. 另外你这个如果应用在orm里datatable -> 实体,如果表或字段一多,cache的键值对是相当的庞大吧,不知道有没有更好的方法。
     回复 引用   
  43. #43楼[楼主] Jeffrey Zhao      2009-02-12 21:23
    @guozili
    可以尽量减少缓存项——而且其实好的数据结构还是一样的,不会影响性能。
     回复 引用 查看   
  44. #44楼 googleman2009-02-22 08:30
    老赵好!

    我的您的一位忠实观众朱先忠(googleman)。想向您请教一个问题(其实,很早就有这个想法),是关于微软ASP.NET AJAX客户端框架的未来前途的。
     回复 引用   
  45. #45楼 lightning[未注册用户]2009-03-29 23:59
    其实可以使用委托的...这样的话,性能会提高很多,而且能工作在.NET 2.0上

     回复 引用   
  46. #46楼[楼主] Jeffrey Zhao      2009-03-30 00:08
    @lightning
    如果您能看一下文章的内容再回复我会很感激的……
     回复 引用 查看   
  47. #47楼 lightning[未注册用户]2009-04-01 20:26
    @Jeffrey Zhao
    其实我的意思是一些静态方法和属性,可以直接用Delegate.CreateDelegate创建委托
     回复 引用   
  48. #48楼[楼主] Jeffrey Zhao      2009-04-01 22:59
    --引用--------------------------------------------------
    lightning: @Jeffrey Zhao
    其实我的意思是一些静态方法和属性,可以直接用Delegate.CreateDelegate创建委托
    --------------------------------------------------------
    这和静态不静态有什么关系……你还是没有看清这个解决方案的作用,我再举个例子吧。
    Delegate.CreateDelegate的作用是什么呢?是把R Method(T1, T2, T3)变成一个Func<T1, T2, T3, R>的委托对象,但是这又有什么用呢?调用起来还是必须依次传递对象,不能替代反射的作用。
    反射的接口是什么呢?object Invoke(object instance, object[] args),可以把参数作为数组传入,得到结果,这个是最灵活的动态调用,很多情况下无可避免。我的解决方案,就是保持这种调用方法不变的情况下,大大提高性能。
    因此,我还是希望您可以仔细看一下文章,以及它引用的内容,这样可以避免很多误会……
     回复 引用 查看   
  49. #49楼 lightning[未注册用户]2009-04-02 20:11
    @Jeffrey Zhao
    但是在很多情况下,我们是知道实例类型,还有参数类型和返回值类型的,这样不如直接使用泛型委托
     回复 引用   
  50. #50楼[楼主] Jeffrey Zhao      2009-04-02 20:17
    @lightning
    1、我没有听懂你的意思。
    2、就算是“很多时候”像你说的那样,那么我的解决方案就是面向另外一种功能的——您说这么多的意义何在呢?既然不能替代,为什么说我的做法“不如XXXX”?

    真的,希望您可以仔细看一下文章再评论,这些解释都是可以省下的,最没有意义的口舌之争……
     回复 引用 查看   
  51. #51楼 kyorry      2009-04-07 08:54
    老赵,您很长时间没去更新了,什么时候能发布一个1.0版本啊,后来您又做了一个性能计数器,下一版本Sample会不会也加进去
     回复 引用 查看   
  52. #52楼 信徒~[未注册用户]2009-09-09 18:40
    老赵 真乃神人也 膜拜!
     回复 引用   
  53. #53楼 小No      2009-11-20 12:41
    @Jeffrey Zhao
    老赵,有个老外用了你这个组件之后啊,发了一张帖子说你这个组件在多线程下有可能有问题

    以下是这个帖子的地址:
    http://fastreflectionlib.codeplex.com/Thread/View.aspx?ThreadId=48730

    老赵不知道你看过没有,能不能确认一下是否有这个问题
     回复 引用 查看   
  54. #54楼[楼主] Jeffrey Zhao      2009-11-20 13:10
    @小No
    很久以前的问题了,现在没问题。
     回复 引用 查看   
  55. #55楼 小No      2009-11-20 14:31
    @Jeffrey Zhao
    OK, 谢谢!!
     回复 引用 查看   
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1380991 nhlfweLJxm4=