再谈Attribute性能优化方式:使用CCI Metadata
2009-11-20 10:47 by 老赵, 6628 visitsAttribute使用了反射,密集调用时会带来较大开销,因此我们上次提出了一些优化方式,这样就不会产生性能方面的问题了。这个优化方式的关键,主要是使用直接获得构造Attribute的元数据,然后自定义它们的生成方式并缓存,这样就避免了每次获取元数据及反射构造Attribute的开销。我从一开始就抱有这个优化的“思路”,但是上篇文章中最终的做法是受到了heros同学的提示才得出的,因为我一开始还根本不知道CustomAttributeData这个已然内置的类库。我当时在探索的方向是使用CCI Metadata读取程序集中与Attribute相关的元数据。
我们知道,Attribute的数据是在编译期就确定的,它虽然涉及到构造函数,涉及到属性赋值,但是无论是构造函数的参数还是属性的值,都是在编译期已然确定的——例如常量,例如typeof。代码在编译成程序集之后,这些数据就存放在程序集的元数据中,我们可以直接通过既定的格式“读取”而不用“加载”到程序中。
那么谁会去读取它呢?需要的程序就会去读,例如:编译器。编译器在编译一段代码时,需要了解代码引用了哪些程序集,是否使用了程序集中正确的成员。被引用的程序集可能很多又很大,因此不可能“加载”进来再来判断,因此它读取的其实便是程序集的“元数据”。还是以Attribute打个比方,如果我的代码使用了某个Attribute,那么编译器便会通过“元数据”去查看这个Attribute的AttributeUsageAttribute标记,检查它的AllowMultiple属性,以此判断我的用法是否正确。其他一些使用场景还包括程序集的静态检查工具等等,例如著名的FxCop。那么FxCop是如何读取程序集元数据的呢?它便使用了CCI相关组件。
CCI(Common Compiler Infrastructure)相关组件有两个:CCI Metadata和CCI 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的动力了。如果您感兴趣,也不妨研究并分享一下。这方面的内容似乎全世界范围内都很少见。
嗯,没错,您一定发现了,这篇文章其实只是记录了一个花絮。
沙发了??