Hello World
Spiga

Attribute操作的性能优化方式

2009-11-18 10:09 by 老赵, 19582 visits

Attribute是.NET平台上提供的一种元编程能力,可以通过标记的方式来修饰各种成员。无论是组件设计,语言之间互通,还是最普通的框架使用,现在已经都离不开Attribute了。迫于Attribute的功能的重要性(Kent Beck认为NUnit比早期JUnit设计的好,一个主要方面便是利用了Attribute),Java语言也在5.0版本中引入了与Attribute类似的Annotation概念。不过Attribute说到底也是一种反射操作,平时正常使用不会带来问题,但是密集的调用还是对性能有一定影响的。这次我们就来总结看看我们究竟可以如何回避Attribute操作的一些性能问题。

假设我们有一个Attribute,它定义在一个类型上:

[AttributeUsage(AttributeTargets.Class,
    AllowMultiple = true,
    Inherited = true)]
public class TestAttribute : Attribute
{
    public TestAttribute(string prop)
    {
        this.Prop = prop;
    }

    public TestAttribute() { }

    public string Prop { get; set; }
}

[Test("Hello World")]
[Test(Prop = "Hello World")]
public class SomeClass { }

那么,如果我们需要获得SomeClass类型上所标记的TestAttribute,我们一般会使用Type对象的GetCustomAttributes方法。那么在其中又发生了什么呢?

通过.NET Reflector来追踪其中实现,会发现这些逻辑最终是由CustomAttribute的GetCustomAttributes方法完成的,感兴趣的朋友们可以找到那个最复杂的重载。由于实现有些复杂,我没有看懂完整的逻辑,但从关键的代码上可以看出,它其实是使用了Activator.CreateInstance方法创建对象,并且使用反射对Attribute对象的属性进行设置。于是我便打算了解一下这些反射操作占整个GetCustomAttributes方法的多大比重:

CodeTimer.Time("GetCustomAttributes", 1000 * 100, () =>
{
    var attributes = typeof(SomeClass).GetCustomAttributes(typeof(TestAttribute), true);
});

CodeTimer.Time("Reflection", 1000 * 100, () =>
{
    var a1 = (TestAttribute)Activator.CreateInstance(typeof(TestAttribute), "Hello World");
    var a2 = (TestAttribute)Activator.CreateInstance(typeof(TestAttribute));
    typeof(TestAttribute).GetProperty("Prop").SetValue(a2, "Hello World", null);
});

结果如下:

GetCustomAttributes
        Time Elapsed:   2,091ms
        CPU Cycles:     5,032,765,488
        Gen 0:          43
        Gen 1:          0
        Gen 2:          0

Reflection
        Time Elapsed:   527ms
        CPU Cycles:     1,269,399,624
        Gen 0:          40
        Gen 1:          0
        Gen 2:          0

可以看出,虽然GetCustomAttributes方法中使用了反射进行对象的创建和属性设置,但是它的大部分开销还是用于获取一些元数据的,它们占据了3/4的时间,而反射的开销其实只占了1/4左右。这就有些令人奇怪了,既然是静态的元数据,为什么.NET Framework不对这些数据进行缓存,而是每次再去取一次呢?即便是我们不应该缓存最后得到的Attribute对象,但是用于构造对象的“信息”是完全可以缓存下来的。

事实上,经由上次heros同学指出,.NET Framework事实上已经给出了足够的信息,那便是CustomAttributeData的GetCustomAttributes方法,它返回的是IList<CustomAttributeData>对象,其中包含了构造Attribute所需要的全部信息。换句话说,我完全可以根据一个CustomAttributeData来“快速构建”Attribute对象:

public class AttributeFactory
{
    public AttributeFactory(CustomAttributeData data)
    {
        this.Data = data;

        var ctorInvoker = new ConstructorInvoker(data.Constructor);
        var ctorArgs = data.ConstructorArguments.Select(a => a.Value).ToArray();
        this.m_attributeCreator = () => ctorInvoker.Invoke(ctorArgs);

        this.m_propertySetters = new List<Action<object>>();
        foreach (var arg in data.NamedArguments)
        {
            var property = (PropertyInfo)arg.MemberInfo;
            var propertyAccessor = new PropertyAccessor(property);
            var value = arg.TypedValue.Value;
            this.m_propertySetters.Add(o => propertyAccessor.SetValue(o, value));
        }
    }

    public CustomAttributeData Data { get; private set; }

    private Func<object> m_attributeCreator;
    private List<Action<object>> m_propertySetters;

    public Attribute Create()
    {
        var attribute = this.m_attributeCreator();

        foreach (var setter in this.m_propertySetters)
        {
            setter(attribute);
        }

        return (Attribute)attribute;
    }
}

AttributeFactory利用了FastReflectionLib,将ConstructorInfo和PropertyInfo封装成性能很高的ConstructorInvoker和PropertyAccessor对象,这样使用起来便有数量级的性能提高。我们再来进行一番测试:

var factories = CustomAttributeData.GetCustomAttributes(typeof(SomeClass))
    .Where(d => d.Constructor.DeclaringType == typeof(TestAttribute))
    .Select(d => new AttributeFactory(d)).ToList();

CodeTimer.Time("GetCustomAttributes", 1000 * 100, () =>
{
    var attributes = typeof(SomeClass).GetCustomAttributes(typeof(TestAttribute), true);
});

CodeTimer.Time("AttributeFactory", 1000 * 100, () => factories.ForEach(f => f.Create()));

结果如下:

GetCustomAttributes
        Time Elapsed:   2,131ms
        CPU Cycles:     5,136,848,904
        Gen 0:          43
        Gen 1:          43
        Gen 2:          0

Attribute Factory
        Time Elapsed:   18ms
        CPU Cycles:     44,235,564
        Gen 0:          4
        Gen 1:          4
        Gen 2:          0

在这里,我们先获得SomeClass中所有定义过的CustomAttributeData对象,然后根据其Constructor的类型来判断哪些是用于构造TestAttribute对象的,然后用它们来构造AttributeFactory。在实际使用过程中,AttributeFactory实例可以缓存下来,并反复使用。这样的话,我们即可以每次得到新的Attribute对象,又可以避免GetCustomAttributes方法所带来的莫名其妙的开销。

事实上,我们完全可以利用这个方法,来实现一个性能更高的GetCustomAttributesEx方法,它的行为可以和.NET自带的GetCustomAttributes完全一致,但是性能可以快上无数——可能是100倍。不过,这个方法虽然不难编写,但比较麻烦。因为CustomAttributeData只能用于获得“直接定义”在某个成员上的数据,而实际情况是,我们往往还必须根据某个Attribute上标记的AttributeUsage的AllowMultiple和Inherited属性来决定是否要遍历整个继承链。只有这般,我们才能百分之百地重现GetCustomAttribute方法的行为。

不过我们在这里有个优势,那便是“静态”。一旦“静态”,我们便可以为某个特定的场景,用“肉眼”判断出特定的处理方式,这样便不需要一个非常通用的GetCustomAttributeEx方法了。例如在实际使用过程中,我们可以可以发现某个Attribute的Inherited属性为false,那么我们便可以免去遍历继承链的麻烦。

最后还有两点可能值得一提:

除了Type,Assembly等成员自带的GetCustomAttributes方法之外,Attribute类也有些静态GetCustomAttributes方法可用于获取Attribute对象。但是,通过.NET Reflector,我们可以发现,Attribute类中的静态方法,最终还是委托给各自的实例方法,因此不会有性能提高。唯一区别对待的是ParameterInfo——不过我没搞懂为什么那么复杂,感兴趣的朋友可以自行探索一番。

如果仅仅是判断一个成员是否定义了某个特定类型的Attribute对象,那么可以使用Attribute.IsDefined静态方法。它的性能比GetCustomAttributes后再判断数组的Length要高效许多倍。不过个人认为这点倒并不是非常重要,因为这原本就是个静态的信息,即便是我们使用较慢的GetCustomAttributes方法来进行判断,也可以把最终的true或false结果进行缓存,这自然也不会有性能问题了。

我们之所以要反复调用GetCustomAttributes方法,就是因为每次得到的Attribute对象都是新建的,因此在某些场景下可能无法缓存它们。不过现在已经有了现在更快的做法,在这方面自然也就不会有太大问题了。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

59 条回复

  1. 随碟附送的
    *.*.*.*
    链接

    随碟附送的 2009-11-18 10:11:00

    老赵后面的那个暴牙是谁啊?

  2. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-11-18 10:11:00

    板凳

  3. 老赵
    admin
    链接

    老赵 2009-11-18 10:16:00

    @随碟附送的
    大家关注美女好不好……

  4. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:19:00

    先做人,再做技术人员,最后做程序员。
    改下, 先说人 ,再做事, 在做技术 ,最后做程序员。

    对于atrribute方式 的 反射点用,我感觉在很多情况下意义不大。
    如果是为了特定的框架,特定实现 就可以了。
    atrribute 为的是 可以在不需要的时候不影响 系统的 运行。

  5. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:20:00

    后面的美女,把她给潜了吧。哈哈

  6. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:21:00

    在说一句 ,老赵 ,你的blog性能 浏览比其它人的慢不少啊。有意见了。

  7. Jeff Wong
    *.*.*.*
    链接

    Jeff Wong 2009-11-18 10:21:00

    老赵,你的TestAttribute特性是AttributeTargets.Class的,不知道AttributeTargets为属性,方法等等的情况下你有没有测试过?

  8. 老赵
    admin
    链接

    老赵 2009-11-18 10:21:00

    rockshit:
    先做人,再做技术人员,最后做程序员。
    改下, 先说人 ,再做事, 在做技术 ,最后做程序员。

    对于atrribute方式 的 反射点用,我感觉在很多情况下意义不大。
    如果是为了特定的框架,特定实现 就可以了。
    atrribute 为的是 可以在不需要的时候不影响 系统的 运行。


    汗一下,两段话都没有看懂。

  9. Gnie
    *.*.*.*
    链接

    Gnie 2009-11-18 10:23:00

    老赵换照片了,和那个紧身衣相比有些反弹了吧~

  10. 老赵
    admin
    链接

    老赵 2009-11-18 10:25:00

    @Jeff Wong
    除了Type之外我只试过ParameterInfo了,结果和文章里的差不多。
    不过,无论是MethodInfo还是ParameterInfo,Assembly之类的,最终都是用了CustomAttribute的GetCustomAttributes方法,性能应该差不多吧。

  11. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:25:00

    @Jeffrey Zhao

    Jeffrey Zhao:

    rockshit:
    先做人,再做技术人员,最后做程序员。
    改下, 先说人 ,再做事, 在做技术 ,最后做程序员。

    对于atrribute方式 的 反射点用,我感觉在很多情况下意义不大。
    如果是为了特定的框架,特定实现 就可以了。
    atrribute 为的是 可以在不需要的时候不影响 系统的 运行。


    汗一下,两段话都没有看懂。


    第一段 ,是你的博客的宗旨 ,看你的页面的左上角。
    第二段, 获取atrribute 属性的时候 ,特意去提高性能 ,在实际应用中意义不大。atrribute 目的是,可以把它当作没有, 不调用它,不影响程序各种性能。

  12. lion.net[未注册用户]
    *.*.*.*
    链接

    lion.net[未注册用户] 2009-11-18 10:26:00

    暴牙?哈哈 那哥们听到了得吓一跳 平时不暴的 这照片照的有水平 印象中是一位MVP 在Tech.ED上还聊了会

  13. 老赵
    admin
    链接

    老赵 2009-11-18 10:26:00

    rockshit:在说一句 ,老赵 ,你的blog性能 浏览比其它人的慢不少啊。有意见了。


    可惜我没法优化,有机会和dudu去提提。你用什么浏览器?

  14. 老赵
    admin
    链接

    老赵 2009-11-18 10:28:00

    @rockshit
    宗旨就不说了,还是没明白,为啥要改?说Attribute。
    我就是因为实际项目中遇到性能问题了,所以要优化Attribute性能。
    如果它不会成为问题,那么自然不需要优化,呵呵。
    大概8成是不需要的吧,我这篇文章面向那2成需求。

  15. 老赵
    admin
    链接

    老赵 2009-11-18 10:30:00

    rockshit:后面的美女,把她给潜了吧。哈哈


    不认识啊,没办法。

  16. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-11-18 10:30:00

    Jeffrey Zhao:
    @随碟附送的
    大家关注美女好不好……


    美女看不清,有大图吗?还是那个龅牙gg搞笑

  17. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:30:00

    Jeffrey Zhao:

    rockshit:在说一句 ,老赵 ,你的blog性能 浏览比其它人的慢不少啊。有意见了。


    可惜不是我没法优化,有机会和dudu去提提。你用什么浏览器?


    ie8 firefox3.5
    可能是公司网速的问题
    你右侧的内容比较丰富

  18. 老赵
    admin
    链接

    老赵 2009-11-18 10:30:00

    Gnie:老赵换照片了,和那个紧身衣相比有些反弹了吧~


    没有,比那时轻了至少5斤了,我头大,看全身就好多了,hmmm……

  19. rockshit
    *.*.*.*
    链接

    rockshit 2009-11-18 10:32:00

    做人 ,人品 ,平的
    做事 , 做事的态度,学习的方法 等
    技术 ,特定的事情
    程序, 特定事情下的特定职业

  20. 老赵
    admin
    链接

    老赵 2009-11-18 10:32:00

    麒麟.NET:
    美女看不清,有大图吗?


    大图在此

  21. 老赵
    admin
    链接

    老赵 2009-11-18 10:33:00

    @rockshit
    听上去有点道理,不过我这句话的起源是傅雷对傅聪说的话……为了追求这“文化”暂时不改了阿,hoho

  22. 过客5678[未注册用户]
    *.*.*.*
    链接

    过客5678[未注册用户] 2009-11-18 10:34:00

    老赵减肥了。
    药物减肥,还是节食减肥,还是运动减肥成功的?

  23. 老赵
    admin
    链接

    老赵 2009-11-18 10:35:00

  24. .netlover
    *.*.*.*
    链接

    .netlover 2009-11-18 10:39:00

    3楼说的那个“暴牙”应该是成都的,什么零码培训的。。。。

  25. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-18 10:50:00

    呵呵,性能差别还真大

  26. 老衲2009[未注册用户]
    *.*.*.*
    链接

    老衲2009[未注册用户] 2009-11-18 10:52:00

    “暴牙”是博客园的 Wilson Wu

  27. 未登录[未注册用户]
    *.*.*.*
    链接

    未登录[未注册用户] 2009-11-18 10:57:00

    老赵,你那照片还是改一下,我估计后面的那位大哥不希望看到这张。。你再看看上面的评论。。。

  28. 老赵
    admin
    链接

    老赵 2009-11-18 11:00:00

    未登录:老赵,你那照片还是改一下,我估计后面的那位大哥不希望看到这张。。你再看看上面的评论。。。


    唉,好不容易找到一张看上去不错的。

  29. 道法自然
    *.*.*.*
    链接

    道法自然 2009-11-18 11:07:00

    我非常喜欢.NET的Attribute,因为它用起来实在太简单了。不过,以前没有关注它的反射性能,老赵还真细心。

    照片的美女确实不错!

  30. 老赵
    admin
    链接

    老赵 2009-11-18 11:09:00

    @道法自然
    不是细心,是正好遇到了这个问题……

  31. 老赵
    admin
    链接

    老赵 2009-11-18 11:18:00

    @上山打老虎
    关于基础性话题,还是看些书吧……被人翻来复去讲了无数遍了。

  32. 上山打老虎
    *.*.*.*
    链接

    上山打老虎 2009-11-18 11:18:00

    看不懂,能不能具体介绍下。

    Attribute都是怎么定义的啊?什么情况下使用啊。

  33. 何厚[未注册用户]
    *.*.*.*
    链接

    何厚[未注册用户] 2009-11-18 11:55:00

    我怎么没看到美女,难道来晚了,照片被撤了??

  34. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-18 12:08:00

    照片被撤了 T_T

  35. Jerry Qian
    *.*.*.*
    链接

    Jerry Qian 2009-11-18 12:19:00

    美女照片呢。

  36. heros
    *.*.*.*
    链接

    heros 2009-11-18 15:40:00

    好些天了。文章终成。
    知道还有更精采的在后面。

  37. 老赵
    admin
    链接

    老赵 2009-11-18 15:45:00

    @heros
    别期望太高啊,我很懒的,用的都是现成的东西……

  38. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-18 16:48:00

    Jeffrey Zhao:
    @heros
    别期望太高啊,我很懒的,用的都是现成的东西……


    牙根有点痒

  39. 老赵
    admin
    链接

    老赵 2009-11-18 17:44:00

    @韦恩卑鄙 alias:v-zhewg
    而且其实现在我自己也不太会,hoho

  40. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-18 23:42:00

    再说我咬你

  41. 老赵
    admin
    链接

    老赵 2009-11-19 00:07:00

    @韦恩卑鄙 alias:v-zhewg
    话说今天睡醒先来点别的无聊的,星期五再来搞这个话题,希望能搞到你们满意,呵呵。

  42. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-19 00:08:00

    我们是那么容易搞得么 还要搞到我们满意

    可恶

    给你介绍个胸大屁股大的女朋友搞吧!

  43. 老赵
    admin
    链接

    老赵 2009-11-19 00:39:00

    @韦恩卑鄙 alias:v-zhewg
    爽的,可以密谈。

  44. virus
    *.*.*.*
    链接

    virus 2009-11-19 09:15:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @韦恩卑鄙 alias:v-zhewg
    爽的,可以密谈。


    还密谈呢,我们都看见了,对了,老赵,我看见你大量使用var定义变量,不知道这样让.NET自己判断类型的方式对性能损耗大吗?是不是就推荐这么使用呢?定义的时候使用var,需要强类型的时候就 as 一下

  45. 老赵
    admin
    链接

    老赵 2009-11-19 09:25:00

    @virus
    我觉得你可以去了解一下var究竟是怎么一回事就明白了……

  46. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-19 11:27:00

    virus:
    @Jeffrey Zhao

    Jeffrey Zhao:
    @韦恩卑鄙 alias:v-zhewg
    爽的,可以密谈。


    还密谈呢,我们都看见了,对了,老赵,我看见你大量使用var定义变量,不知道这样让.NET自己判断类型的方式对性能损耗大吗?是不是就推荐这么使用呢?定义的时候使用var,需要强类型的时候就 as 一下


    简单说 编译的时候慢一点 运行的时候var不var一个样

  47. abcabc[未注册用户]
    *.*.*.*
    链接

    abcabc[未注册用户] 2009-12-01 22:37:00

    老赵问一下下面的代码怎么测呢?希望能给解惑
    [Field(FieldName = "LoginName", FieldType = DbType.String, IsPrimeryKey = false, IsForeighKey = false, IsNullable = false)]
    public string LoginName
    {
    get;set;
    }

  48. 老赵
    admin
    链接

    老赵 2009-12-01 23:07:00

    @abcabc
    想要测什么?

  49. abcabc[未注册用户]
    *.*.*.*
    链接

    abcabc[未注册用户] 2009-12-02 09:22:00

    abcabc:
    老赵问一下下面的代码怎么测呢?希望能给解惑
    [Field(FieldName = "LoginName", FieldType = DbType.String, IsPrimeryKey = false, IsForeighKey = false, IsNullable = false)]
    public string LoginName
    {
    get;set;
    }


    我想知道怎么样用你的 AttributeFactory 取得上面的
    FieldName FieldType 的值,期待老赵的回复

  50. 幸运草
    *.*.*.*
    链接

    幸运草 2009-12-02 09:35:00

    假如想得到所有 Properties FieldAttribute 只能再加上一个循环了,对不对呢?老赵

  51. 老赵
    admin
    链接

    老赵 2009-12-02 10:28:00

    @abcabc
    看懂了我的AttributeFactory,写一个不难吧。

  52. 老赵
    admin
    链接

    老赵 2009-12-02 10:29:00

    @幸运草
    一个AttributeFactory是对应一个Attribute的,实际情况下可以自己写啊,我的代码只是展示性质的,不是标准。

  53. PowerCoder
    *.*.*.*
    链接

    PowerCoder 2009-12-25 12:13:00

    问句题外话老赵:
    var value = arg.TypedValue.Value;
    this.m_propertySetters.Add(o => propertyAccessor.SetValue(o, value));
    这个是不是会把public AttributeFactory(CustomAttributeData data)里的环境变量保存下来因为lambda表达式o => propertyAccessor.SetValue(o, value)用到了AttributeFactory构造函数里面的变量value是不是AttributeFactory的环境变量在AttributeFactory构造函数执行完后不会立即消失?

  54. 老赵
    admin
    链接

    老赵 2009-12-25 12:21:00

    @PowerCoder
    听不懂

  55. PowerCoder
    *.*.*.*
    链接

    PowerCoder 2009-12-26 21:15:00

    var value = arg.TypedValue.Value;
    定义在AttributeFactory的构造函数里面,那么AttributeFactory构造函数执行完后AttributeFactory里面定义的变量应该都会被释放吧?那么this.m_propertySetters.Add(o => propertyAccessor.SetValue(o, value));引用了这个value,那等到public Attribute Create()执行的时候,会调用o => propertyAccessor.SetValue(o, value),那么这个时候value的只是从哪儿来的呢?因为value是属于AttributeFactory构造函数的变量,是不是在AttributeFactory()执行完后就被释放了呢?

  56. 老赵
    admin
    链接

    老赵 2009-12-26 23:39:00

    @PowerCoder
    value被形成闭包了,不会被释放。

  57. pixysoft
    121.32.25.*
    链接

    pixysoft 2010-06-01 00:48:10

    既然ConstructInfo得到了,初始化的参数也得到了,为什么还要对Property去设置,直接构造初始化的Ctor,传入参数,这样逻辑才是正确的。

    因为我发现即使ctor有输入参数,但是不一定等于有Property. 比如:

    public class TestAttribute : Attribute
    {
        public TestAttribute(string prop)
        {
        }
    
        public TestAttribute() { }
    }
    
  58. delphidoc
    61.148.17.*
    链接

    delphidoc 2010-11-03 16:57:38

    Attribute经常使用容易上人上瘾.

    FastReflectionLib怎么现在不继续往下做了,很好的一个东东.

  59. 链接

    培华 2011-02-22 15:36:46

    囧,我来看的时候还是只是那个黑色紧身也的老赵,并没有看到啥美女。 老赵太不老实了,也不留个底给后人看看。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我