一次失败的尝试(上):原来GetCustomAttributes方法每次都返回新的实例
2009-11-10 00:08 Jeffrey Zhao 阅读(22652) 评论(67) 编辑 收藏 举报前一段时间我在比较各种URL生成方式性能的时候,其实已经为利用Lambda表达式的做法进行了优化。在优化之前,使用Lambda构建URL的性能比现在的结果还要慢上50%。性能低下的原因,在于每次都使用GetCustomAttributes来获取参数(或其他一些地方)标记的Custom Attribute。这里应该用到了反射,在这种密集调用情形中性能急转直下。
我当时并没有多想,为什么ASP.NET MVC要每次都使用反射来获取一遍——既然Custom Attribute标记都已经静态编译在类型上了,为什么不“缓存”下每个参数所对应的Custom Attribute呢?既然框架没有这么做,那么我就自己进行缓存吧。于是我在辅助方法里针对每个PropertyInfo对象缓存了它所对应的Custom Attribute(其实是CustomModelBinderAttribute的子类),然后每次都可以调用GetBinder方法来获取IModelBinder实例。经过这样的“优化”之后,性能的确有了很大提高。
既然缓存了CustomModelBinderAttribute,那么如系统自带的ModelBinderAttribute那样每次都根据binderType来新建一个IModelBinder对象也没有太大必要了。因为我们几乎所有的IModelBinder都是不依赖上下文的,它的同一个对象可以被多个线程同时调用。如果不是修改了参数状态,它们基本上可以算作是“无副作用”的“纯(pure)函数”。为了减少对象创建或回收时消耗的时间,我写了一个BinderAttribute:
[AttributeUsage(ValidTargets, AllowMultiple = false, Inherited = false)] public class BinderAttribute : CustomModelBinderAttribute { public BinderAttribute(Type binderType) : this(binderType, true) { } public BinderAttribute(Type binderType, bool singleton) { if (binderType == null) { throw new ArgumentNullException("binderType"); } if (!typeof(IModelBinder).IsAssignableFrom(binderType)) { var message = String.Format("{0} doesn't implement IModelBinder.", binderType.FullName); throw new ArgumentException(message, "binderType"); } this.BinderType = binderType; var creator = GetBinderCreator(binderType); if (singleton) { var instance = creator(); this.m_binderGetter = () => instance; } else { this.m_binderGetter = creator; } } private static Func<IModelBinder> GetBinderCreator(Type binderType) { var newExpr = Expression.New(binderType); var castExpr = Expression.Convert(newExpr, typeof(IModelBinder)); var lambdaExpr = Expression.Lambda<Func<IModelBinder>>(castExpr); return lambdaExpr.Compile(); } public Type BinderType { get; private set; } private readonly Func<IModelBinder> m_binderGetter; public override IModelBinder GetBinder() { return this.m_binderGetter(); } }
BinderAttribute的特点在于,它会根据构造函数的singleton参数,来决定是每次都返回同一个还是构造新的实例。在“默认”情况下,singleton为true,这满足大部分情况下“纯”的Model Binder。但如果在万中无一的情况下出现了“只能使用一次”的Model Binder实现,那么您也可以将singleton参数设为false,这样便可以每次都返回新的实例了。此外,即便是返回新的实例,我也使用了表达式树构造的委托来创建新对象,性能比原来的Activator.CreateInstance要高出许多。
但事实上,这种做法并不可行,因为我的“前提”就错了。我的前提是,GetCustomAttributes每次返回的都是同样的对象。换句话说,我以为对于每个Custom Attribute标记,只会创建一个对象,然后多次返回,多次复用。但是经过试验,GetCustomAttributes方法事实上每次都会返回新的对象。这意味着,即便每个BinderAttribute对象各维护一个可复用的IModelBinder对象(如果singleton为true),但如果我们每次都获得新的BinderAttribute对象,这还是达不到singleton的效果。在ASP.NET MVC的场景下,我们的确可以缓存各个CustomModelBinderAttribute(目前也是这么做的),但是这和GetCustomAttributes方法相比还是改变了原有的行为。如果某人在代码里使用了“只能使用一次”的CustomModelBinderAttribute实现,那么我们的“优化”方式从严格意义上来说是不合格的。
因此,MvcPatch并不应该“毫无顾忌”地缓存IModelBinder或CustomModelBinderAttribute对象。那么我们又该怎么办呢?这就是另一个话题了,下次再说吧。