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 =
string name1 = someInstance.Name;
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
PropertyDescriptor
s 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) {
baseMethod = typeof(ChainingPropertyDescriptor).GetMethod("GetValue");
mb = tb.DefineMethod(baseMethod.Name,
MethodAttributes.HideBySig | MethodAttributes.Public |
MethodAttributes.Virtual | MethodAttributes.Final,
baseMethod.CallingConvention, baseMethod.ReturnType, new Type[] {
typeof(object) });
il = mb.GetILGenerator();
if (property.DeclaringType.IsValueType) {
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 {
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Castclass, property.DeclaringType);
}
il.Emit(OpCodes.Callvirt, property.GetGetMethod());
if (property.PropertyType.IsValueType) {
il.Emit(OpCodes.Box, property.PropertyType);
}
il.Emit(OpCodes.Ret);
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
ICustomTypeDescriptor
s 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) {
ICustomTypeDescriptor descriptor = base.GetTypeDescriptor(objectType,
null);
descriptors.Add(objectType, descriptor);
try {
descriptor = new HyperTypeDescriptor(descriptor);
descriptors[objectType] = descriptor;
return descriptor;
} catch {
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;
Type wrapMe = Assembly.GetAssembly(typeof(PropertyDescriptor)).
GetType("System.ComponentModel.ReflectPropertyDescriptor");
foreach (PropertyDescriptor oldProp in oldProps) {
PropertyDescriptor pd = oldProp;
if (ReferenceEquals(wrapMe, pd.GetType()) &&
TryCreatePropertyDescriptor(ref pd)) {
changed = true;
}
newProps[index++] = pd;
}
return changed ? new PropertyDescriptorCollection(newProps, true) :
oldProps;
}
}
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 OpCount
s 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