代码改变世界

一次失败的尝试(上):原来GetCustomAttributes方法每次都返回新的实例

2009-11-10 00:08 by Jeffrey Zhao, ... 阅读, ... 评论, 收藏, 编辑

前一段时间我在比较各种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对象。那么我们又该怎么办呢?这就是另一个话题了,下次再说吧。

相关文章

使用Live Messenger联系我