Hello World
Spiga

再谈Attribute性能优化方式:使用CCI Metadata

2009-11-20 10:47 by 老赵, 6642 visits

Attribute使用了反射,密集调用时会带来较大开销,因此我们上次提出了一些优化方式,这样就不会产生性能方面的问题了。这个优化方式的关键,主要是使用直接获得构造Attribute的元数据,然后自定义它们的生成方式并缓存,这样就避免了每次获取元数据及反射构造Attribute的开销。我从一开始就抱有这个优化的“思路”,但是上篇文章中最终的做法是受到了heros同学的提示才得出的,因为我一开始还根本不知道CustomAttributeData这个已然内置的类库。我当时在探索的方向是使用CCI Metadata读取程序集中与Attribute相关的元数据。

我们知道,Attribute的数据是在编译期就确定的,它虽然涉及到构造函数,涉及到属性赋值,但是无论是构造函数的参数还是属性的值,都是在编译期已然确定的——例如常量,例如typeof。代码在编译成程序集之后,这些数据就存放在程序集的元数据中,我们可以直接通过既定的格式“读取”而不用“加载”到程序中。

那么谁会去读取它呢?需要的程序就会去读,例如:编译器。编译器在编译一段代码时,需要了解代码引用了哪些程序集,是否使用了程序集中正确的成员。被引用的程序集可能很多又很大,因此不可能“加载”进来再来判断,因此它读取的其实便是程序集的“元数据”。还是以Attribute打个比方,如果我的代码使用了某个Attribute,那么编译器便会通过“元数据”去查看这个Attribute的AttributeUsageAttribute标记,检查它的AllowMultiple属性,以此判断我的用法是否正确。其他一些使用场景还包括程序集的静态检查工具等等,例如著名的FxCop。那么FxCop是如何读取程序集元数据的呢?它便使用了CCI相关组件。

CCI(Common Compiler Infrastructure)相关组件有两个:CCI MetadataCCI Code and AST。这两个组件由微软研究院构建,现在都已经在CodePlex上开源。CCI Metadata的作用是用于读取和写入CLR程序集和pdb文件。它的作用和System.Reflection和System.Reflection.Emit有些类似,但它们最关键的一点不同是CCI Metadata直接读取“文件”,而System.Reflection需要“加载”。与CCI Code and AST组件功能相对应的就类似于System.CodeDom了,它会将一个程序集和pdb文件读取成层级化的,树状的对象结构。当然,它也不用加载程序集。从理论上说,CCI的这两个组件构成了.NET Reflector的基础功能。换句话说,你有需要像.NET Reflector那样读取程序集的原数据和IL代码,甚至要去简单了解IL的含义吗?那么可以参考CCI的这两个组件。

不过,微软似乎从来没有想过要发布这两个组件,毕竟太小众了,而且一旦发布压力就大了,例如要在升级前后保证API的兼容性等等。从它们的源代码来,至今还在不断修改(这星期还都有check in)。不过它们也不是没有稳定的发布,它便是CCI Samples项目,我们可以下载那个80几兆的压缩包,其中包括了所有的源代码、构建信息,文档和开发工具(如xUnit)——还有多余svn的托管数据(删除这些数据后其实只有不到40兆,真是浪费)。在下载了代码之后,便可以打开其中的解决方案(其中高亮的GeneralXp项目是我自己的试验项目,本不包含在解决方案中):

Metadata目录中包含的便是CCI Metadata项目,而直接放在解决方案中的便是CCI Code and AST了。在这里,我们并不关心具体的Code和AST,因为我们只需要知道Attribute的数据,属于元数据,因此我们只需要使用CCI Metadata。

假设我们需要获取的是这样的Attribute信息:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class TestAttribute : Attribute
{
    public TestAttribute(string s)
    {
        Console.WriteLine(s);
    }

    public TestAttribute(Type type)
    {
        Console.WriteLine(type);
    }
}

[Test(typeof(string))]
[Test("Hello World")]
public class SomeClass { }

首先,我们需要创建一个HostEnvironment:

internal class HostEnvironment : MetadataReaderHost
{
    private PeReader peReader;

    internal HostEnvironment()
        : base(new NameTable(), 4)
    {
        this.peReader = new PeReader(this);
    }

    public override IUnit LoadUnitFrom(string location)
    {
        var document = BinaryDocument.GetBinaryDocumentForFile(location, this);
        var unit = this.peReader.OpenModule(document);
        this.RegisterAsLatest(unit);
        return unit;
    }
}

然后,便可以用它来获得构建Attribute对象的工厂委托:

static void Main(string[] args)
{
    var type = typeof(SomeClass);
    var attrType = typeof(TestAttribute);

    // 打开SomeClass所在程序集
    HostEnvironment host = new HostEnvironment();
    var assemblyFile = type.Assembly.Location;
    var assembly = host.LoadUnitFrom(assemblyFile) as IAssembly;

    // 找到SomeClass的原数据
    var typeDef = assembly.GetAllTypes().Single(
        t => TypeHelper.GetTypeName(t) == type.FullName);

    // 获得TestAttribute元数据
    var attrDefs = typeDef.Attributes.Where(
        t => TypeHelper.GetTypeName(t.Type) == attrType.FullName);

    // 构建工厂委托
    var factories = attrDefs.Select(a => GetAttributeFactory(a)).ToList();
    factories.ForEach(f => f());

    Console.WriteLine("press enter to continue...");
    Console.ReadLine();
}

static Func<Attribute> GetAttributeFactory(ICustomAttribute attrDef)
{
    var type = Type.GetType(TypeHelper.GetTypeName(attrDef.Type));
    var args = attrDef.Arguments.Select(a => GetArgumentValue(a)).ToArray();

    return () => (Attribute)Activator.CreateInstance(type, args);
}

static object GetArgumentValue(IMetadataExpression expression)
{
    // 如果这个参数是常量
    var constant = expression as IMetadataConstant;
    if (constant != null) return constant.Value;

    // 如果这个参数是typeof
    var typeGet = expression as IMetadataTypeOf;
    if (typeGet != null)
    {
        return Type.GetType(TypeHelper.GetTypeName(typeGet.TypeToGet));
    }

    throw new NotSupportedException();
}

这段代码是可以工作的(不过还不支持对Attribute参数的设置)。但是,如果您想要我解释API的使用方式或者更具体的细节,我也说不出,而且要不是MetadataHelper中的那些辅助函数(如TypeHelper类),我根本也无法在短时间内写出这段代码。

由于CCI并不会加载程序集,因此所有对的元数据的访问都是陌生的类型,例如使用ITypeReference来表示一个类型,ICustomAttribute表示一个Attribute定义,以及IMetadataExpression来表示一个表达式。由于我现在已经知道了CustomAttributeData这个方便好用的类库,因此也已经没有深入CCI的动力了。如果您感兴趣,也不妨研究并分享一下。这方面的内容似乎全世界范围内都很少见。

嗯,没错,您一定发现了,这篇文章其实只是记录了一个花絮。

Creative Commons License

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

Add your comment

36 条回复

  1. 极品拖拉机
    *.*.*.*
    链接

    极品拖拉机 2009-11-20 10:53:00

    沙发了??

  2. 感激[未注册用户]
    *.*.*.*
    链接

    感激[未注册用户] 2009-11-20 10:57:00

    感谢老赵,看你的博客学到很多东西...

  3. 极品拖拉机
    *.*.*.*
    链接

    极品拖拉机 2009-11-20 10:58:00

    没怎么看明白,有什么优势吗?

  4. 老赵
    admin
    链接

    老赵 2009-11-20 11:03:00

    @极品拖拉机
    优势总是相对的,你是问在解决哪个问题上,和哪种做法相比有没有优势?

  5. 假如爱有天意
    *.*.*.*
    链接

    假如爱有天意 2009-11-20 11:07:00

    高深~

  6. heros
    *.*.*.*
    链接

    heros 2009-11-20 11:22:00

    heros想手工去分析binary方式读取的程序集信息,google不到什么资料,无奈自己半瓶水,没头绪。好东西,谢谢了。

  7. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-20 11:39:00

    泪奔。。。。。CCI又是第一次听说。。。。。

  8. heros
    *.*.*.*
    链接

    heros 2009-11-20 11:44:00

    @Ivony...
    《你不知道的.NET》看来很有必要。:)你们赶紧执笔吧。我定拜读。

  9. 梁利锋
    *.*.*.*
    链接

    梁利锋 2009-11-20 11:53:00

    也可以参考一下 PostSharp

  10. 老赵
    admin
    链接

    老赵 2009-11-20 12:04:00

    @heros
    看你的博客还以为你是搞JS的……

  11. 老赵
    admin
    链接

    老赵 2009-11-20 12:05:00

    @梁利锋
    它主要是读写IL吧,很有趣的AOP实现方式。

  12. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-20 12:06:00

    heros:
    @Ivony...
    《你不知道的.NET》看来很有必要。:)你们赶紧执笔吧。我定拜读。




    我是打算写一篇关于接口的东东了。

  13. 破浪
    *.*.*.*
    链接

    破浪 2009-11-20 12:35:00

    《你不知道的.NET》我Google了下,没找到呢。能否给个链接看看

  14. 老赵
    admin
    链接

    老赵 2009-11-20 12:43:00

    @破浪
    你没看清我们在说什么,呵呵。

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

    道法自然 2009-11-20 12:47:00

    确实不错,其实除了这玩意,还有一个Mono.cecil项目也可完成类似功能。对于AOP,以后在.NET就可以不用声明Virtual了,:)。我是在搜索“查看元数据而又避免加载程序集”关键字的时候,搜索到Mono.cecil的。

  16. Zhenway
    *.*.*.*
    链接

    Zhenway 2009-11-20 12:52:00

    ms版的cecil?以前就玩过cecil

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

    道法自然 2009-11-20 13:29:00

    cecil本来就支持MS。

  18. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-11-20 13:29:00

    Ivony...:泪奔。。。。。CCI又是第一次听说。。。。。


    那我裸奔算了。。。。。。

  19. 梁利锋
    *.*.*.*
    链接

    梁利锋 2009-11-20 13:30:00

    @Jeffrey Zhao
    PostSharp 确实是读写 IL 实现 AOP,所以,它生成的 Assembly 就不需要使用反射来操作,所以速度上来说,和直接写代码类似,也不需要运行时 Emit 代码。
    另外,自己为它写“插件”的话,也可以实现其他不属于 AOP 范畴的事。

  20. HEHEHEHE[未注册用户]
    *.*.*.*
    链接

    HEHEHEHE[未注册用户] 2009-11-20 13:36:00

    偶的第一个念头就是这东东可以拿来做静态织入

  21. 老赵
    admin
    链接

    老赵 2009-11-20 13:38:00

    @梁利锋
    嗯嗯,一直觉得它这种做法很有意思,呵呵。

  22. heros
    *.*.*.*
    链接

    heros 2009-11-20 13:50:00

    刚才把cecil cci postsharp都下了下来。粗略的看了下,都有在直接读元数据表信息,具体代码没看,也看不懂。cecil postsharp都能读写il直接修改assembly静态文件,由于现在还不了解具体,所以有两个疑问,一个运行期间的程序集如何实现被修改?如果有强名称的程序集是否支持或如何被修改?

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

    道法自然 2009-11-20 14:53:00

    heros:刚才把cecil cci postsharp都下了下来。粗略的看了下,都有在直接读元数据表信息,具体代码没看,也看不懂。cecil postsharp都能读写il直接修改assembly静态文件,由于现在还不了解具体,所以有两个疑问,一个运行期间的程序集如何实现被修改?如果有强名称的程序集是否支持或如何被修改?



    虽然没有试验,但是按我的理解,在运行时插入IL应该是没有问题,但是没有办法去更改强名称的程序集。可以做个实验,验证一下。

  24. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-20 15:45:00

    梁利锋:
    @Jeffrey Zhao
    PostSharp 确实是读写 IL 实现 AOP,所以,它生成的 Assembly 就不需要使用反射来操作,所以速度上来说,和直接写代码类似,也不需要运行时 Emit 代码。
    另外,自己为它写“插件”的话,也可以实现其他不属于 AOP 范畴的事。




    读写IL?那意味着不能用强名了?

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

    道法自然 2009-11-20 16:07:00

    @Ivony...

    强名称是在加载是检验的吧?运行时都已经加载到内存了,更改应该没有问题。做个实验就得了。

  26. 梁利锋
    *.*.*.*
    链接

    梁利锋 2009-11-20 17:01:00

    Ivony...:

    梁利锋:
    @Jeffrey Zhao
    PostSharp 确实是读写 IL 实现 AOP,所以,它生成的 Assembly 就不需要使用反射来操作,所以速度上来说,和直接写代码类似,也不需要运行时 Emit 代码。
    另外,自己为它写“插件”的话,也可以实现其他不属于 AOP 范畴的事。




    读写IL?那意味着不能用强名了?


    印象中是可以用强名的,因为PostSharp的读写IL发生在编译时,它可以从sln文件得到snk的路径,然后再写入的。

  27. 梁利锋
    *.*.*.*
    链接

    梁利锋 2009-11-20 17:05:00

    @heros
    cecil 不知道,不过 PostSharp 的写入发生在编译时,准确的说它是编译后的二次编译,所以跟运行时没什么关系。

  28. Zhenway
    *.*.*.*
    链接

    Zhenway 2009-11-20 17:22:00

    @道法自然
    那是当然的。。。我回帖的时候,还没看到你的回复,呵呵
    我说的是老赵提到的那个cci就是ms版的cecil
    ps:用cecil写过一个代码混淆器,蛮好玩的

  29. JamesX[未注册用户]
    *.*.*.*
    链接

    JamesX[未注册用户] 2009-11-21 11:48:00

    最近老趙談的主題雖然我覺得很棒,但是對我來說有些生疏。

    工作上都是做一些應用面的開發,底層知識不是那麼熟悉,對於最近的主題有種找不到切入點的感覺。

    常有看完一整篇還是不知道在講什麼的遺憾。

  30. 老赵
    admin
    链接

    老赵 2009-11-21 12:30:00

    @JamesX
    这些真能算是底层吗?排开这篇其他和性能相关的都是纯粹应用层面的东西吧。

  31. JamesX[未注册用户]
    *.*.*.*
    链接

    JamesX[未注册用户] 2009-11-21 18:04:00

    可能是自己學的用的真的太淺了,或許不該用底層來形容,"深層"或許比較適合。

    以"再談Attribute性能優化方式:使用CCI Metadata"這個題目來說,必須先去了解一下Attribute的運作方式,為什麼會需要優化,再來看老趙的文章會比較有收獲。

    而這些自備的智識點,不能只有泛泛的認識,否則看這篇文章還是有點吃力。

    而且文章中又會串連出更多智識點,例如在說明CCI是什麼時"它的作用和System.Reflection和System.Reflection.Emit有些類似",那可能又必須去"深入"了解Reflection及Emit,否則不能體老趙在文中對它們之間的比較。

    老趙對技術深入研究的態度令我很敬佩,我也長期在看這個博客,只是最近常遇到有些的題目會找不到切入點。

    例如這個智識點可以用在那裡呢?
    值不值得研究下去呢?
    即使看不懂,將來或許用的到,值不值得收藏呢?

    比如這篇文章的目的是否是用在控件開發的性能調優?那就能算在控制開發的進階文章,假如對控件開發有興趣的話,將來可以再回頭來看這篇文章。

  32. 景裔
    *.*.*.*
    链接

    景裔 2009-11-22 14:34:00

    上个星期刚研究过Attribute,想用它来做点什么,不过没有头绪,后来放弃了。看了这个后又被引发心魔了……

  33. dark
    *.*.*.*
    链接

    dark 2009-11-22 22:01:00

    昨天晚上刚好在看微软的Attribute例子.
    敲了些代码.但弄不明白....

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class TestAttribute : Attribute
    {
        public TestAttribute(string s)
        {
            Console.WriteLine(s);
        }
    
        public TestAttribute(Type type)
        {
            Console.WriteLine(type);
        }
    }
    
    [Test(typeof(string))]
    [Test("Hello World")]   //这里的Test是如何知道是TestAttribute类的 难道只要定义成 OOAttribute这样的然后下面要用到的话 就 [OO("构造参数")] 这样的吗?
    public class SomeClass { }
    

  34. 老赵
    admin
    链接

    老赵 2009-11-22 22:02:00

    @dark
    这应该是基础吧……

  35. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-23 00:17:00

    dark:
    昨天晚上刚好在看微软的Attribute例子.
    敲了些代码.但弄不明白....

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class TestAttribute : Attribute
    {
        public TestAttribute(string s)
        {
            Console.WriteLine(s);
        }
    
        public TestAttribute(Type type)
        {
            Console.WriteLine(type);
        }
    }
    
    [Test(typeof(string))]
    [Test("Hello World")]   //这里的Test是如何知道是TestAttribute类的 难道只要定义成 OOAttribute这样的然后下面要用到的话 就 [OO("构造参数")] 这样的吗?
    public class SomeClass { }
    




    其实MSDN上的示例和说明很详细。。。。
    在应用Attribute时。Attribute类型的Attribute后缀可以省略,编译器在找不到Test : Attribute时,就会找TestAttribute : Attribute。。。

    其实这个思想在当时应该说是还是蛮超前的。。。。

  36. 老赵
    admin
    链接

    老赵 2009-11-23 00:45:00

    @Ivony...
    嗯嗯,其实这就是“约定”,不是吗?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我