65.9K
CodeProject 正在变化。 阅读更多。
Home

HyperDescriptor:加速动态属性访问

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (56投票s)

2007 年 4 月 18 日

7分钟阅读

viewsIcon

237236

downloadIcon

3290

提供了一个大大加速的运行时属性实现,甚至可以应用于闭源类。

引言

.NET 提供了灵活的数据绑定和运行时属性访问,但默认情况下是通过反射实现的,而反射被认为是相对较慢的。本文利用 Reflection.Emit 的强大功能,为反射属性提供了一个预编译(且大大加速)的实现,并演示了如何使用 TypeDescriptionProvider 将此实现动态应用于类型。

文章包含大量技术细节,但所有代码都已包含在源代码中;作为使用者,您几乎无需做什么。真的。您可能希望直接跳转到使用场景部分,然后如果您想了解其工作原理,再回头查看详细信息。

背景

数据绑定(以及各种其他运行时访问)使用了 System.ComponentModel;这是类型或实例声明“我拥有这些属性”的方式。每个属性都表示为一个 PropertyDescriptor,它提供了有关底层类型、名称的信息,以及(最重要的)用于允许访问数据的 GetValue()SetValue() 方法。默认情况下,框架使用反射来提供对类型定义的属性的访问——但有一个问题:反射(相对而言)是缓慢的(下文将量化)。如果您使用大量数据绑定,或者需要在运行时动态访问属性,这可能会成为一个瓶颈。

具体来说,以下(“示例 1”,“示例 2”)实现了相同的功能,但性能差异巨大

public class MyEntity {
    private string name;
    public event EventHandler NameChanged;
    public string Name {
        get {return name;}
    }
    set {
        if (value != Name) {
            name = value;
            EventHandler handler = NameChanged;
            if (handler != null) handler(this, EventArgs.Empty);
        }
    }
}
//...

MyEntity someInstance = //TODO
// SAMPLE 1: compiled property access
string name1 = someInstance.Name;

// SAMPLE 2: runtime property access (standard pattern using 
//TypeDescriptor)
string name2 = (string) TypeDescriptor.GetProperties(someInstance)
    ["Name"].GetValue(someInstance);

后者必须进行大量调用来验证参数(因为一切都被键入为 object),并且需要做很多工作才能在运行时正确调用属性的 getter。我们的目标是尽可能消除这种开销。

幸运的是,反射并非故事的终结。在 1.1 版本中,框架支持 ICustomTypeDescriptor 接口;通过将一个实例传递给 GetProperties(),系统可以查询此接口并补充属性。这类似于 DataRow 如何公开与 DataTable 列匹配的绑定属性,而不是 DataRow 类本身的属性。但这仍然不是理想的。

  • 它要求实例实现复杂的 ICustomTypeDescriptor 接口(工作量很大)。
  • 它不能应用于您无法控制的类型。
  • 当询问的是类型而不是实例时,它无法正常工作。

.NET 2.0 进一步改进了这一点;通过使用 TypeDescriptionProvider,我们可以将运行时类型信息的提供委托给单独的类。更重要的是,我们可以在运行时提供提供程序——这意味着我们可以有效地扩展/替换可用的属性。太棒了。

PropertyDescriptor 实现

为了改变性能,我们的最终目标是让“示例 2”内部运行“示例 1”,而不是使用反射。对于单个已知类型,这相对容易,虽然枯燥;我们只需要为该类型的每个属性创建一个 PropertyDescriptor 类,并在编译时执行强制转换。

public sealed class MyEntityNamePropertyDescriptor : ChainingPropertyDescriptor
{
public MyEntityNamePropertyDescriptor(PropertyDescriptor parent) : 
    base(parent) {}
public override object GetValue(object component) {
    return (string) ((MyEntity)component).Name;
}
public override void SetValue(object component, object value) {
    ((MyEntity)component).Name = (string)value;
}
public override bool IsReadOnly {
    get { return false; }
}
public override bool SupportsChangeEvents {
    get { return true; }
}
public override void AddValueChanged(object component, EventHandler handler) {
    ((MyEntity)component).NameChanged += handler;
}
public override void RemoveValueChanged(object component, EventHandler handler)
 {
    ((MyEntity)component).NameChanged -= handler;
}        
}

(在这里,ChainingPropertyDescriptor 是一个简单的 PropertyDescriptor 实现,它通过调用父类实现的许多方法来支持链式调用。)

然而,这种方法显然只适用于我们提前知道的类型和属性,即使这样,保持所有内容最新也是一个噩梦。这时 Reflection.Emit 就派上用场了;这是一种元编程机制——也就是说,我们可以(在运行时)创建一组类似于上述的类。尽管其他方法(如 CodeDom)也是可行的,但(在我看来) Reflection.Emit 是最简洁的。不幸的是,它要求您使用 IL 进行编码。我不是 IL 专家,所以我的高科技方法是编写 4 个如下所示的 PropertyDescriptor,并在 ILDASM 和 Reflector 中查看生成的 IL

  • 具有类属性的类实体
  • 具有结构体属性的类实体
  • 具有类属性的结构体实体
  • 具有结构体属性的结构体实体

幸运的是,这些简单方法的 IL 非常简单;以 GetValue() 为例(尽管此处不提供 IL 的完整解释)

MethodBuilder mb;
MethodInfo baseMethod;
if (property.CanRead) {
    // obtain the implementation that we want to override
    baseMethod = typeof(ChainingPropertyDescriptor).GetMethod("GetValue");
    // create a new method that accepts an object and returns an object 
    // (as per the base)
    mb = tb.DefineMethod(baseMethod.Name,
        MethodAttributes.HideBySig | MethodAttributes.Public | 
            MethodAttributes.Virtual | MethodAttributes.Final,
        baseMethod.CallingConvention, baseMethod.ReturnType, new Type[] { 
            typeof(object) });
    // start writing IL into the method
    il = mb.GetILGenerator();
    if (property.DeclaringType.IsValueType) {
        // unbox the object argument into our known (instance) struct type
        LocalBuilder lb = il.DeclareLocal(property.DeclaringType);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Unbox_Any, property.DeclaringType);
        il.Emit(OpCodes.Stloc_0);
        il.Emit(OpCodes.Ldloca_S, lb);
    } else {
        // cast the object argument into our known class type
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Castclass, property.DeclaringType);
    }
    // call the "get" method
    il.Emit(OpCodes.Callvirt, property.GetGetMethod());
    if (property.PropertyType.IsValueType) {
        // box it from the known (value) struct type
        il.Emit(OpCodes.Box, property.PropertyType);
    }
    // return the value
    il.Emit(OpCodes.Ret);
    // signal that this method should override the base
    tb.DefineMethodOverride(mb, baseMethod);
}

这对于其他引用的重写方法也是重复的(请注意,然而,我们只重写了类的 SetValue()AddValueChanged()RemoveValueChanged(),因为对于结构体来说,在拆箱过程中更改的目的是会丢失的;在这些情况下,只需将调用委托给原始的反射实现)。

TypeDescriptionProvider 实现

TypeDescriptionProvider 的工作是返回一个 ICustomTypeDescriptor 来描述指定的类型/实例。由于我们目标是反射类,因此我们可以专注于类型(而不是实例),并且由于我们使用动态类型创建,因此我们希望重用任何生成的类。为此,我们将为我们被询问到的每种类型缓存一个特定的 ICustomTypeDescriptor。为了获得一个起点,我们将再次使用链式调用(这在基础构造函数的重载中受支持)——也就是说,我们可以使用给定类型的先前定义的提供程序。作为最后的手段,我们将简单地使用全局提供程序——也就是说,为“object”定义的提供程序。

sealed class HyperTypeDescriptionProvider : TypeDescriptionProvider {
public HyperTypeDescriptionProvider() : this(typeof(object)) { }
public HyperTypeDescriptionProvider(Type type) : 
    this(TypeDescriptor.GetProvider(type)) { }
public HyperTypeDescriptionProvider(TypeDescriptionProvider parent) : 
    base(parent) { }
public static void Clear(Type type) {
    lock (descriptors) {
        descriptors.Remove(type);
    }
}
public static void Clear() {
    lock (descriptors) {
        descriptors.Clear();
    }
}
private static readonly Dictionary<Type, ICustomTypeDescriptor> descriptors
= new Dictionary<Type, ICustomTypeDescriptor>();

public sealed override ICustomTypeDescriptor GetTypeDescriptor(Type objectType,
    object instance) {
    ICustomTypeDescriptor descriptor;
    lock (descriptors) {
        if (!descriptors.TryGetValue(objectType, out descriptor)) {
            try {
                descriptor = BuildDescriptor(objectType);
            } catch {
                return base.GetTypeDescriptor(objectType, instance);
            }
        }
        return descriptor;
    }
}
[ReflectionPermission( SecurityAction.Assert, 
    Flags = ReflectionPermissionFlag.AllFlags)]
private ICustomTypeDescriptor BuildDescriptor(Type objectType) {
    // NOTE: "descriptors" already locked here

    // get the parent descriptor and add to the dictionary so that
    // building the new descriptor will use the base rather than recursing
    ICustomTypeDescriptor descriptor = base.GetTypeDescriptor(objectType, 
        null);
    descriptors.Add(objectType, descriptor);
    try {
        // build a new descriptor from this, and replace the lookup
        descriptor = new HyperTypeDescriptor(descriptor);
        descriptors[objectType] = descriptor;
        return descriptor;
    } catch {
        // rollback and throw
        // (perhaps because the specific caller lacked permissions;
        // another caller may be successful)
        descriptors.Remove(objectType);
        throw;
    }
}

ICustomTypeDescriptor 实现

再次,链式调用是救星。我们不需要实现所有内容——只需要我们想更改的部分。具体来说,我们将要求基础描述符提供它知道的属性,然后我们将检查它们是否看起来像反射。

sealed class HyperTypeDescriptor : CustomTypeDescriptor {
    private readonly PropertyDescriptorCollection properties;
    internal HyperTypeDescriptor(ICustomTypeDescriptor parent) : 
        base(parent) {
        properties = WrapProperties(parent.GetProperties());
    }
    public sealed override PropertyDescriptorCollection GetProperties(
        Attribute[] attributes) {
        return properties;
    }
    public sealed override PropertyDescriptorCollection GetProperties() {
        return properties;
    }
    private static PropertyDescriptorCollection WrapProperties(
        PropertyDescriptorCollection oldProps) {
        PropertyDescriptor[] newProps = new PropertyDescriptor[oldProps.Count];
        int index = 0;
        bool changed = false;
        // HACK: how to identify reflection, given that the class is internal
        Type wrapMe = Assembly.GetAssembly(typeof(PropertyDescriptor)).
            GetType("System.ComponentModel.ReflectPropertyDescriptor");
        foreach (PropertyDescriptor oldProp in oldProps) {
            PropertyDescriptor pd = oldProp;
            // if it looks like reflection, try to create a bespoke descriptor
            if (ReferenceEquals(wrapMe, pd.GetType()) && 
                TryCreatePropertyDescriptor(ref pd)) {
                changed = true;
            }
            newProps[index++] = pd;
        }

        return changed ? new PropertyDescriptorCollection(newProps, true) : 
            oldProps;
    }
    // TryCreatePropertyDescriptor not shown, but flavor indicated previously
}

使用代码

到这个时候就容易多了。有两种主要方法可以将我们的新提供程序挂接到对象模型中;第一种,通过 TypeDescriptionProviderAttribute

[TypeDescriptionProvider(typeof(HyperTypeDescriptionProvider))]
public class MyEntity {
    // ...
}

显然,这只适用于我们控制的类型,但非常有表现力。不必在子类型上包含该属性,因为 TypeDescriptor 会自动查找祖先来解析提供程序。

第二种方法是通过 TypeDescriptor.AddProvider()。这同样支持继承,但为了使我们的生活更轻松(通过链式调用等),我们可以公开一个辅助函数。

sealed class HyperTypeDescriptionProvider : TypeDescriptionProvider {
    public static void Add(Type type) {
        TypeDescriptionProvider parent = TypeDescriptor.GetProvider(type);
        TypeDescriptor.AddProvider(new HyperTypeDescriptionProvider(parent), 
            type);
    }
    // ...
}
// ...
HyperTypeDescriptionProvider.Add(typeof(MyEntity));

性能

那么它的性能如何呢?我们不妨使用大量的运行时属性访问(以便即使是最粗略的计时器也变得合理),看看它在更改前后(参见示例)的表现如何。特别是,我们将测量执行时间。

  • 对于直接(硬编码)访问(为简洁起见,针对单个属性)
    • 属性获取
    • 属性设置
    • 事件添加/删除
    • 操作数(作为一切都已发生的检查)
  • 对于间接(System.ComponentModel)访问(针对多个属性,包括继承)
    • GetProperties
    • IsReadOnly
    • SupportsChangeEvents
    • GetValue
    • SetValue(如果支持)
    • AddHandler/RemoveHandler(如果支持)
    • 操作数(作为一切都已发生的检查)

这是发布版本的结果。

Direct access
    MyEntity.Name    GetValue    8ms
    MyEntity.Name    SetValue    97ms
    MyEntity.Name    ValueChanged    1022ms
  OpCount: 25000000

Without HyperTypeDescriptionProvider
    MyEntity.Name    GetProperties    647ms
    MyEntity.Name    IsReadOnly    2926ms
    MyEntity.Name    SupportsChangeEvents    245ms
    MyEntity.Name    GetValue    10360ms
    MyEntity.Name    SetValue    20288ms
    MyEntity.Name    ValueChanged    29566ms
  OpCount: 25000000
    MySuperEntity.Name    GetProperties    828ms
    MySuperEntity.Name    IsReadOnly    2881ms
    MySuperEntity.Name    SupportsChangeEvents    241ms
    MySuperEntity.Name    GetValue    10682ms
    MySuperEntity.Name    SetValue    20730ms
    MySuperEntity.Name    ValueChanged    30979ms
  OpCount: 25000000
    MySuperEntity.When    GetProperties    825ms
    MySuperEntity.When    IsReadOnly    2888ms
    MySuperEntity.When    SupportsChangeEvents    251ms
    MySuperEntity.When    GetValue    11393ms
    MySuperEntity.When    SetValue    22416ms
  OpCount: 10000000

With HyperTypeDescriptionProvider
    MyEntity.Name    GetProperties    699ms
    MyEntity.Name    IsReadOnly    43ms
    MyEntity.Name    SupportsChangeEvents    41ms
    MyEntity.Name    GetValue    57ms
    MyEntity.Name    SetValue    155ms
    MyEntity.Name    ValueChanged    954ms
  OpCount: 25000000
    MySuperEntity.Name    GetProperties    914ms
    MySuperEntity.Name    IsReadOnly    41ms
    MySuperEntity.Name    SupportsChangeEvents    44ms
    MySuperEntity.Name    GetValue    95ms
    MySuperEntity.Name    SetValue    173ms
    MySuperEntity.Name    ValueChanged    1059ms
  OpCount: 25000000
    MySuperEntity.When    GetProperties    891ms
    MySuperEntity.When    IsReadOnly    41ms
    MySuperEntity.When    SupportsChangeEvents    46ms
    MySuperEntity.When    GetValue    295ms
    MySuperEntity.When    SetValue    110ms
  OpCount: 10000000

首先——请注意 OpCount 在前后都是匹配的,所以我们完成了相同数量的有效工作。例如,NameGetValue 从 >10 秒减少到 <100 毫秒——实际上,速度提高了约 180 倍。对于 When,由于装箱而产生了一个额外的固定成本,但仍然有 40 倍的改进。然而,更重要的是与直接(已编译)访问的比较;我们几乎达到了!将这些数字作为直接已编译访问的倍数来看,“get”、“set”和“event”访问分别约为 7、1.5 和 1,这非常令人满意(后者是由于 Delegate 操作的固定开销)。元数据(GetProperties)访问是一个有趣的现象;在某些机器上更快,在某些机器上更慢,所以我们认为它是持平的。尽管如此,这是一个巨大的进步。

真正令人欣喜的是,您可以随意使用它,可以少量使用,也可以大量使用;要么将其应用于您在网格中绑定的那个重要类,要么将其应用于 System.Windows.Forms.Control 看看会发生什么(我没有尝试过;人们想知道这是否可以提高 Visual Studio 的性能,特别是依赖于 PropertyGridSystem.ComponentModel 的设计器)。我绝对会远远避免将其应用于 object

摘要

  • 运行时反射数据访问速度大大加快
  • 可应用于现有(外部)类和新构建
  • 提供了一个附加功能的框架

历史

  • 2007 年 4 月 20 日:改进了指标;纠正了类名(!);调整了以考虑 ReflectionPermission(Assert 和故障安全)——感谢 Josh Smith
  • 2007 年 4 月 15 日:支持更改通知。改进了示例输出。使用父类的 SupportsChangeEvents 和 IsReadOnly 答案。修订了命名空间和类名。
  • 2007 年 4 月 13 日:第一个版本
© . All rights reserved.