代码改变世界

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

2009-11-20 10:47 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

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的动力了。如果您感兴趣,也不妨研究并分享一下。这方面的内容似乎全世界范围内都很少见。

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