A few weeks ago I was facing some performance issues with a DeepCopy class I wrote. The goal of this class is to provide a faster way for deep-copying an object graph rather than serializing and deserializing with BinaryFormatter. The performance analysis of my code showed that getting or setting a field via
FieldInfo.GetValue and
FieldInfo.SetValue was very slow. So I searched for a possibility to access fields in a faster way. I found some interesting things, but not exactly what i was looking for.
The Idea
I got the basic idea from this page:
Jachman Wordpress - 2000% faster using dynamic method calls/
It describes a way of accessing properties with dynamic methods. The difference between properties and fields is that for accessing a property you have to call it's get- or set-method, when accessing a field the C# compiler will use special IL instructions to get or set the value.
Analyse the IL code (Getter)
The fastest way to access a field is to directly access it:
public class FieldAccessorsExample
{
public string _field1;
public int _field2;
}
// access
var instance = new SomeClass();
var value = instance._someField;
To get high performance getters and setters, we should have some methods which will imitate the direct access. I thought of something like this:
public static object Getter(object source)
{
return ((FieldAccessorsExample)source)._field1;
}
After compiling the code above, we can view the IL code with ILDisassembler from Visual Studio. To understand the IL code, I found the documentation of the
OpCodes class very helpful.
.method public hidebysig static object Getter(object source) cil managed
{
// Code size 12 (0xc)
.maxstack 8
IL_0000: ldarg.0
IL_0001: castclass FieldSetterGetter.FieldAccessorsExample
IL_0006: ldfld string FieldSetterGetter.FieldAccessorsExample::_field1
IL_000b: ret
} // end of method FieldAccessorsExample::Getter
For accessing value types the method looks a little bit different.
.method private hidebysig static object ValueTypeGetter(object source) cil managed
{
// Code size 17 (0x11)
.maxstack 8
IL_0000: ldarg.0
IL_0001: castclass FieldSetterGetter.FieldAccessorsExample
IL_0006: ldfld int32 FieldSetterGetter.FieldAccessorsExample::_field2
IL_000b: box [mscorlib]System.Int32
IL_0010: ret
} // end of method FieldAccessorsExample::ValueTypeGetter
With this as the basic template I created the method which will generate our get-function.
public static Func<object, object> GetGetter(FieldInfo fieldInfo)
{
// create a method without a name, object as result type and one parameter of type object
// the last parameter is very import for accessing private fields
var method = new DynamicMethod(string.Empty, typeof(object), new []{ typeof(object) }, fieldInfo.Module, true);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // load the first argument onto the stack (source of type object)
il.Emit(OpCodes.Castclass, fieldInfo.DeclaringType); // cast the parameter of type object to the type containing the field
il.Emit(OpCodes.Ldfld, fieldInfo); // store the value of the given field on the stack. The casted version of source is used as instance
if(fieldInfo.FieldType.IsValueType)
il.Emit(OpCodes.Box, fieldInfo.FieldType); // box the value type, so you will have an object on the stack
il.Emit(OpCodes.Ret); // return the value on the stack
return (Func<object, object>)method.CreateDelegate(typeof(Func<object, object>));
}
Analyse the IL code (Setter)
Now we will just do the same for the set-function. First a simple function which could set the value on a given instance.
public void Setter(object target, object value)
{
((FieldAccessorsExample)target)._field1 = (string)value;
}
The IL code generated by the compiler is:
.method public hidebysig static void Setter(object target,
object 'value') cil managed
{
// Code size 18 (0x12)
.maxstack 8
IL_0000: ldarg.0
IL_0001: castclass FieldSetterGetter.FieldAccessorsExample
IL_0006: ldarg.1
IL_0007: castclass [mscorlib]System.String
IL_000c: stfld string FieldSetterGetter.FieldAccessorsExample::_field1
IL_0011: ret
} // end of method FieldAccessorsExample::Setter
Or the following for value types:
.method public hidebysig static void ValueTypeSetter(object target,
object 'value') cil managed
{
// Code size 18 (0x12)
.maxstack 8
IL_0000: ldarg.0
IL_0001: castclass FieldSetterGetter.FieldAccessorsExample
IL_0006: ldarg.1
IL_0007: unbox.any [mscorlib]System.Int32
IL_000c: stfld int32 FieldSetterGetter.FieldAccessorsExample::_field2
IL_0011: ret
} // end of method FieldAccessorsExample::ValueTypeSetter
From this we can create the function, which will return our setter function:
public static Action<object, object> GetSetter(FieldInfo fieldInfo)
{
var method = new DynamicMethod(string.Empty, null, new[] { typeof(object), typeof(object) }, fieldInfo.Module, true);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // load the first argument onto the stack (source of type object)
il.Emit(OpCodes.Castclass, fieldInfo.DeclaringType); // cast the parameter of type object to the type containing the field
il.Emit(OpCodes.Ldarg_1); // push the second argument onto the stack (this is the value)
if (fieldInfo.FieldType.IsValueType)
il.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType); // unbox the value parameter to the value-type
else
il.Emit(OpCodes.Castclass, fieldInfo.FieldType); // cast the value on the stack to the field type
il.Emit(OpCodes.Stfld, fieldInfo); // store the value on stack in the field
il.Emit(OpCodes.Ret); // return
return (Action<object, object>)method.CreateDelegate(typeof(Action<object, object>));
}
Usage
If you put the two functions above in a factory class, the usage could look like this:
var getter = FieldAccessorFactory.GetGetter(fieldInfo);
var setter = FieldAccessorFactory.GetSetter(fieldInfo);
var oldValue = getter(instance);
setter(instance, "newvalue");
Some helpful informations can also be found by searching with google for "open delegates" in C#.
This
page explains the basics.
Performance
10,000,000 Reads
With Reflection: 3.77 sec
With DynamicMethod: 0.17 sec
Factor: 21.9
10,000,000 Writes
With Reflection: 5,14 sec
With DynamicMethod: 0.25
Factor: 20.7
If you have any suggestions or questions, comment.