HyperDescriptor:加速动态属性访问






4.96/5 (56投票s)
2007 年 4 月 18 日
7分钟阅读

237236

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
在前后都是匹配的,所以我们完成了相同数量的有效工作。例如,Name
的 GetValue
从 >10 秒减少到 <100 毫秒——实际上,速度提高了约 180 倍。对于 When
,由于装箱而产生了一个额外的固定成本,但仍然有 40 倍的改进。然而,更重要的是与直接(已编译)访问的比较;我们几乎达到了!将这些数字作为直接已编译访问的倍数来看,“get”、“set”和“event”访问分别约为 7、1.5 和 1,这非常令人满意(后者是由于 Delegate
操作的固定开销)。元数据(GetProperties
)访问是一个有趣的现象;在某些机器上更快,在某些机器上更慢,所以我们认为它是持平的。尽管如此,这是一个巨大的进步。
真正令人欣喜的是,您可以随意使用它,可以少量使用,也可以大量使用;要么将其应用于您在网格中绑定的那个重要类,要么将其应用于 System.Windows.Forms.Control
看看会发生什么(我没有尝试过;人们想知道这是否可以提高 Visual Studio 的性能,特别是依赖于 PropertyGrid
和 System.ComponentModel
的设计器)。我绝对会远远避免将其应用于 object
。
摘要
- 运行时反射数据访问速度大大加快
- 可应用于现有(外部)类和新构建
- 提供了一个附加功能的框架
历史
- 2007 年 4 月 20 日:改进了指标;纠正了类名(!);调整了以考虑 ReflectionPermission(Assert 和故障安全)——感谢 Josh Smith
- 2007 年 4 月 15 日:支持更改通知。改进了示例输出。使用父类的 SupportsChangeEvents 和 IsReadOnly 答案。修订了命名空间和类名。
- 2007 年 4 月 13 日:第一个版本