![]() |
Languages »
C# »
Reflection
Intermediate
HyperDescriptor: Accelerated dynamic property accessBy Marc GravellProvides a vastly accelerate runtime property implementation that can be applied even to closed-source classes |
C#2.0, Windows, .NET2.0, Visual-Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
.NET provides flexible data-binding and runtime property access, but by default
this is via reflection an is known to be relatively slow. This article uses
the power of Reflection.Emit to provide a pre-compiled (and much accelerated) implementation
for reflection properties, and demonstrates the use of TypeDescriptionProvider to
dynamically apply this implementation to types.
A lot of technical details are included, but this code is all provided in the source; as a consumer you have to do almost nothing. Really. You may wish to jump ahead to the usage scenarios, then dip back if you want to know what makes it all work.
Data-binding (and various other runtime access) uses the System.ComponentModel; this is
how a type or instance says "I have these properties." Each property is expressed as a PropertyDescriptor,
which provides information on the underlying type, the name, and (most importantly) a GetValue() and SetValue()
method to allow access to the data. By default the framework uses
reflection to provide access to the properties defined against the types - but there
is a problem: reflection is (relatively speaking) slow (quantified below). If you use a lot
of data binding, or need to dynamically access properties at runtime, then this can be
a drain.
Specifically, the following ("SAMPLE 1", "SAMPLE 2") achieve the same thing, but perform very differently:
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);
The latter must make a myriad of calls to verify the arguments (since everything is typed as object), and must do a lot of work to correctly call the property getter at runtime. Our objective is to eliminate as much of this overhead as possible.
Reflection, fortunately, is not the end of the story. In 1.1, the Framework supported the
ICustomTypeDescriptor interface; by passing an instance to GetProperties(), the system can query this
interface and supplement the properties. This is similar to how a DataRow exposes binding properties that match
the columns of the DataTable, instead of the properties on the DataRow class. But again, it is not ideal:
ICustomTypeDescriptor interface (a lot of work)
.NET 2.0 takes this further; by using a TypeDescriptionProvider, this allows us to delegate provision
of runtime type information to separate classes. More: we can actually supply providers at runtime -
meaning we can effective extend / replace the properties available. Neat.
In order to change the performance, our end-game is to make "SAMPLE 2" run "SAMPLE 1" internally rather
than using reflection. For a single known type, this is relatively easy, if boring; we simply need to
create a PropertyDescriptor class for each property on the type, and perform the casts as compile-time:
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;
}
}
(Here, ChainingPropertyDescriptor is a simple PropertyDescriptor implementation that supports chaining by invoking the
parent's implementation for each of the many methods.)
However, this approach is clearly only possible for types and properties we know about in advance, and even then
it would be a nightmare to keep it all up to date. That is where Reflection.Emit comes in; this is a
mechanism for meta-programming - i.e. we can (at runtime) create a set of classes just like the above.
Although other approaches (like CodeDom) are feasible, Reflection.Emit is (to my mind) the cleanest to use.
Unfortunately, it requires you to code in IL. I am not an IL expert, so my high-tech approach was to write 4
PropertyDescriptors like above, and look at the generated IL in ILDASM and Reflector:
Fortunately, the IL for these simple methods is quite simple; to illustrate with GetValue() (although a full
explanation of IL is not given here):
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);
}
This is repeated for the other cited overrides (note, however, that we only override
SetValue(), AddValueChanged() and RemoveValueChanged()
for classes, since for structures the purpose of the change would lost during unboxing; in these
cases simply defer to the original reflection implementation).
The job of the TypeDescriptionProvider is to return an ICustomTypeDescriptor that
describes the specified type/instance. As we are aiming at reflective classes we
can focus on types (not instances), and since we are using dynamic type creation
we want to re-use any generated classes. To do this we will simply cache a specific
ICustomTypeDescriptors for each type we are asked about. In order to get a starting
point we will again use chaining (this is supported on a base ctor overload) - i.e. we
could use the previously defined provider
for a given type. As a final measure, we will simply use the global provider - i.e.
the provider that is defined for "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;
}
}
Again, chaining to the rescue. We don't need to implement everything - just the bits we want to change. Specifically, we'll ask the base descriptor about the properties *it* knows about, and then we'll inspect them for something that smells like reflection:
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
}
At this point it gets much easier. There are two primary ways of hooking our new provider into the object model; first,
via TypeDescriptionProviderAttribute:
[TypeDescriptionProvider(typeof(HyperTypeDescriptionProvider))]
public class MyEntity {
// ...
}
Obviously this is only suited to types under our control, but is very expressive. It is not
necessary to include the attribute on sub-types since TypeDescriptor automatically
looks at ancestors to resolve a provider.
The second approach is via TypeDescriptor.AddProvider(). Again this supports
inheritance, but to make our lives easier (with chaining, etc.) we can expose a helper:
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));
So how does it perform? How about we use runtime property access a great many times (to make even the crudest timers reasonable) and see how it behaves with and without the change (see sample). In particular, we will measure the time to execute:
System.ComponentModel) access (against several properties, including inheritance):
Here's the results from a release build:
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
First - note the OpCounts all match up before and after, so we are achieving the same amount of
useful work. GetValue on Name, for instance has gone from >10s to <100ms - in fact about 180 times faster.
For When there is an additional fixed cost from boxing, but still a 40-fold improvement.
More important, however, is the comparison to direct (compiled) access; we are nearly there!
Looking at the figures as a factor or direct compiled access gives ~7, ~1.5and ~1 for "get", "set" and "event" access, which
is very satisfactory (the latter being due to the fixed overhead of Delegate manipulations). Metadata (GetProperties) access
is a curiosity; on some machines it is faster, on some slower, so we'll call it even. Still, a vast improvement.
The real joy is you can apply this as sparingly or as liberally as you like; either apply it to that important class that you bind to in a grid, or apply to
System.Windows.Forms.Control and see what happens (I haven't tried this; one wonders
if it might improve the performance of Visual Studio, particularly the designer which
depends heavily on PropertyGrid and the System.ComponentModel). I would
definitely stop well short of applying it to object though.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 20 Apr 2007 Editor: Sean Ewington |
Copyright 2007 by Marc Gravell Everything else Copyright © CodeProject, 1999-2010 Web21 | Advertise on the Code Project |