Click here to Skip to main content
Click here to Skip to main content

HyperDescriptor: Accelerated dynamic property access

, 20 Apr 2007
Rate this:
Please Sign up or sign in to vote.
Provides a vastly accelerate runtime property implementation that can be applied even to closed-source classes

Introduction

.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.

Background

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:

  • It requires the instance to implement the complex ICustomTypeDescriptor interface (a lot of work)
  • It cannot be applied to types outside of your control
  • It does not work when asking about a type rather than an instance

.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.

PropertyDescriptor implementation

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:

  • A class entity with a class property
  • A class entity with a struct property
  • A struct entity with a class property
  • A struct entity with a struct property

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).

TypeDescriptionProvider 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;
    }
}

ICustomTypeDescriptor implementation

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
}

Using the code

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));

Performance

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:

  • For direct (hard-coded) access (against a single property for brevity):
    • property get
    • property set
    • event add/remove
    • Op count (as a check that everything happened)
  • For indirect (System.ComponentModel) access (against several properties, including inheritance):
    • GetProperties
    • IsReadOnly
    • SupportsChangeEvents
    • GetValue
    • SetValue (if supported)
    • AddHandler/RemoveHandler (if supported)
    • Op count (as a check that everything happened)

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.

Summary

  • Much faster runtime reflective data access
  • Applicable to both existing (external) classes and new builds
  • Provides a framework for additional functionality

History

  • 20 Apr 2007: Improved metrics; corrected class name(!); tweaked to take account of ReflectionPermission (Assert and fail-safe) - thanks to Josh Smith
  • 15 Apr 2007: Support change notifications. Improved sample output. Use parent's answer to SupportsChangeEvents and IsReadOnly. Revised namespace and class names
  • 13 Apr 2007: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Marc Gravell
Software Developer Stack Exchange
United Kingdom United Kingdom
Geek at Stack Exchange
Follow on   Twitter

Comments and Discussions

 
QuestionStackoverflow Exception PinmemberMcGiv13-Jan-10 0:41 
AnswerRe: Stackoverflow Exception PinmemberMarc Gravell13-Jan-10 1:22 
AnswerRe: Stackoverflow Exception PinmemberMarc Gravell14-Jan-10 21:50 
GeneralRe: Stackoverflow Exception PinmemberMcGiv14-Jan-10 23:37 
GeneralRe: Stackoverflow Exception Pinmemberavibhor9-Jan-12 8:45 
GeneralRe: Stackoverflow Exception Pinmembermattstermiller17-Jul-12 10:47 
AnswerRe: Stackoverflow Exception [modified] Pinmembermattstermiller17-Jul-12 10:46 
GeneralRe: Stackoverflow Exception [modified] Pinmembermattstermiller5-Mar-13 12:16 
GeneralRe: Stackoverflow Exception PinmemberUberJono14-May-13 22:53 
GeneralRe: Stackoverflow Exception PinmemberFathomSavvy6-Jan-14 10:12 
AnswerRe: Stackoverflow Exception [modified] PinmemberGnomad24-Jul-13 17:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140814.1 | Last Updated 20 Apr 2007
Article Copyright 2007 by Marc Gravell
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid