Background:
I'm currently working on a freelance project to optimize
(yay, optimization again!) a .NET Entity Persister library of a Software
Development firm here in Makati City -- much like a custom built NHibernate,
but heavily dependent on reflection to get/set field values (their library has
no support for Properties). They hired me because they kind of complaining that
their system is so slow, it is taking about several seconds (to minutes) just
to retrieve some thousand records -- considering the internals of their library
is built on top of DataReader (Imagine, looping DataReader to fill a collection of Entities).
According to their big boss, using thrid party O/R solution is not an option, they have to put a
fix to their current library.
Looking at the problem:
Assuming we have an entity class
SampleEntity:
public
class SampleEntity
{
public string
stringField = "Sample Value";
public string
StringProperty { get; set;
}
}
and here's how they are setting the field:
/// <summary>
/// Dynamic field setter
/// </summary>
public
class FieldSetter
{
/// <summary>
/// Set field
dynamically
/// </summary>
public static
void SetField(object
instance,
string fieldName, object
value) {
var t = instance.GetType();
var fieldInfo =
t.GetField(fieldName,
BindingFlags.Instance | BindingFlags.Public);
if (fieldInfo == null)
throw new ArgumentException("There is no publicly accessible " +
fieldName +
" field found in " + t.FullName + ".");
fieldInfo.SetValue(instance, value);
}
}
the library is actually doing this in loop to setup an
entire entity instance, and they repeatedly doing it until the DataReader says stop:
/// <summary>
/// Get all entities from underlying data store.
/// </summary>
public
List<T> SelectAll() {
Open();
var selectAll = new
SqlCommand(BuildSelectStatement<T>(),
Connection);
var reader = selectAll.ExecuteReader();
var list = new
List<T>();
if (reader != null)
while (reader.Read()) {
var entity = default(T);
for (var i = 0; i < reader.FieldCount; i++) {
var
fieldName = reader.GetName(i);
var data
= reader.GetData(i);
FieldSetter.SetField(entity,
fieldName, data);
}
}
Close();
return list;
}
the only problem I've notice that may be the cause of slow execution is the use
of reflection.
Doing it the hard way, and maybe the
faster way:
If you play long enough with reflection, you will get the
experience that it is a slow because of internal loop overheads (search
for matching name and binding attributes).
The approach I thought to workaround the problem is to emit CIL codes directly and call them as dynamic
method, something that look like:
public void SetField(SampleEntity instance, string
value) {
instance.stringField
= value;
}
In CIL, this is how it is coded:
.method public
hidebysig instance
void SetField(
class
SampleEntity instance, string 'value') cil
managed
{
.maxstack
8
SEG_0001: 0x0003
ldarg.0
SEG_0002: 0x0004
ldarg.1
SEG_0003: 0x007
stfld string SampleEntity::stringField
SEG_0008: 0x002
ret
}
The lines I highlighted in red are
the lines I'm interrested in making dynamic, so let's work on a DynamicMethod for doing just that:
// We still need to get the field information from reflection
var
fieldInfo = t.GetField(fieldName,
BindingFlags.Instance | BindingFlags.Public);
if
(fieldInfo == null)
throw new ArgumentException("There
is no publicly accessible " +
fieldName
+
" field found in " + t.FullName + ".");
We only need to get the field information once, during the
setup of our DynamicMethod:
var
setter = new DynamicMethod("__set_field_" + fieldName,
null,
new[] { t, fieldInfo.FieldType },
typeof(FieldSetter));
var
setterIL = setter.GetILGenerator();
This is because we need the FieldInfo as a parameter to stfld:
// ldarg.0 -
load argument 1 to eval.stack.0
setterIL.Emit(OpCodes.Ldarg_0);
// ldarg.1 - load argument 2 to
eval.stack.1
setterIL.Emit(OpCodes.Ldarg_1);
// stfld f -
set the field f of instance stack.0
//
with the value from stack.1
setterIL.Emit(OpCodes.Stfld,
fieldInfo);
//
ret - exit the method
setterIL.Emit(OpCodes.Ret);
If you take a closer look at the emitted OpCodes, you'll see that it is
simply a replica of CIL above, but with a little twist --
the fieldInfo we've taken from given fieldName.
That means we are almost done, we just have to call the
DynamicMethod we've created for assigning value to field like this:
setter.Invoke(null,
new
object[] { t,
"Kamusta mundo, ok ka lang ba?" });
Saving it for succeeding calls:
There's not much improvement at all if we are going to
create DynamicMethod repeatedly, in fact that's even
slower than using reflection. We have to save the created DynamicMethods for succeeding calls.
Saving it to Dictionary and then retrieving it every time we have a need for
it, will boost the performance real fast (because we only have to create DynamicMethod once for every field we want to set
value).
Summing it all together, with addition of controlling the
creation of setter DynamicMethods, we will be able to write a
new
FieldSetter class:
public
class FieldSetter
{
public static
DynamicMethod GetFieldSetterMethod(object instance, string
fieldName) {
var t = instance.GetType();
var key = (t.FullName + "_" + fieldName).Replace(".", "_");
if
(!fieldSetter.ContainsKey(key)) {
lock (fsync) {
// We still need to get the field
information from reflection
var fieldInfo =
t.GetField(fieldName,
BindingFlags.Instance | BindingFlags.Public);
if (fieldInfo == null)
throw new ArgumentException("There is no publicly accessible " +
fieldName +
" field found in " + t.FullName + ".");
var setter = new DynamicMethod("__set_field_" + key, null,
new[] { t, fieldInfo.FieldType },
typeof(FieldSetter));
var setterIL =
setter.GetILGenerator();
// ldarg.0
- load argument 1 to eval.stack.0
setterIL.Emit(OpCodes.Ldarg_0);
// ldarg.1 - load
argument 2 to eval.stack.1
setterIL.Emit(OpCodes.Ldarg_1);
//
stfld f - set the field f of instance stack.0
// with the
value from stack.1
setterIL.Emit(OpCodes.Stfld, fieldInfo);
//
ret - exit the method
setterIL.Emit(OpCodes.Ret);
fieldSetter.Add(key, setter);
}
}
return fieldSetter[key];
}
/// <summary>
/// Set the
field value of given instance, using the name of the field.
/// </summary>
/// <param name="instance">The instance.</param>
/// <param name="fieldName">The name of the field.</param>
/// <param name="value">The value to set.</param>
public static
object SetFieldValue(object
instance,
string fieldName,
object value) {
return
GetFieldSetterMethod(instance, fieldName)
.Invoke(null, new[] {
instance });
}
private static
readonly Dictionary<string, DynamicMethod>
fieldSetter =
new Dictionary<string, DynamicMethod>();
private static
readonly object
fsync = new object();
}
That's about it; I hope you guys find it useful for your own
purpose.
Did it worked?
After I've delivered the code to them, they immediately
tested it.., and tested it.., and tested it more. Long story short, I guess I
made them happy because I've got an extra from the paycheck, and the another
deal to support property on their custom EntityPerstister.
I'll be working more with their library, specifically the adding of support to Property
and Method with one parameter. That's not much of a work left, and I
think I almost got everything (except for the method with one parameter,
but it’s practically the same as Property since properties are methods too!).
More codes:
Here, take a look at the classes I've put-up for
setting/getting field and property (if you don't know how I’ve implemented it,
you can re-read this post, or read more about CIL and DynamicMethod):
//
FieldSetter.cs
public
class FieldSetter
{
private static
DynamicMethod GetFieldSetterMethod(
object instance, string
fieldName) {
var t = instance.GetType();
var key = (t.FullName + "_" + fieldName).Replace(".", "_");
if
(!fieldSetter.ContainsKey(key)) {
lock (fsync) {
// We still
need to get the field information from reflection
var
fieldInfo = t.GetField(fieldName,
BindingFlags.Instance |
BindingFlags.Public);
if
(fieldInfo == null)
throw
new ArgumentException("There is no publicly " +
"accessible " +
fieldName +
" field found in " +
t.FullName + ".");
var setter = new DynamicMethod("__set_field_" + key,
null, new[] { t,
fieldInfo.FieldType },
typeof(FieldSetter));
var setterIL =
setter.GetILGenerator();
// ldarg.0
- load argument 1 to eval.stack.0
setterIL.Emit(OpCodes.Ldarg_0);
// ldarg.1 - load
argument 2 to eval.stack.1
setterIL.Emit(OpCodes.Ldarg_1);
// stfld f - set
the field f of instance stack.0
// with the
value from stack.1
setterIL.Emit(OpCodes.Stfld, fieldInfo);
// ret -
exit the method
setterIL.Emit(OpCodes.Ret);
fieldSetter.Add(key, setter);
}
}
return fieldSetter[key];
}
public static void SetFieldValue(
object
instance, string fieldName, object value) {
GetFieldSetterMethod(instance, fieldName)
.Invoke(null, new[] {
instance, value });
}
private static
readonly Dictionary<string, DynamicMethod>
fieldSetter = new Dictionary<string, DynamicMethod>();
private static
readonly object
fsync = new object();
}
//
FieldGetter.cs
public
class FieldGetter
{
private static
DynamicMethod GetFieldGetterMethod(
object instance, string
fieldName) {
var t = instance.GetType();
var key = (t.FullName + "_" + fieldName).Replace(".", "_");
if
(!fieldGetters.ContainsKey(key)) {
lock (fsync) {
var fieldInfo =
t.GetField(fieldName,
BindingFlags.Instance |
BindingFlags.Public);
if (fieldInfo
== null)
throw
new ArgumentException("There is no publicly " +
"accessible " + fieldName +
" field found in " +
t.FullName + ".");
var getter = new DynamicMethod("__get_field_" + key,
fieldInfo.FieldType, new[] { t },
typeof(FieldGetter));
var getterIL =
getter.GetILGenerator();
//
ldarg.0 - load argument 0 to eval.stack.0
getterIL.Emit(OpCodes.Ldarg_0);
// ldfld f -
get the value of field from
// instance in eval.stack and store it
// on top of eval.stack
getterIL.Emit(OpCodes.Ldfld,
fieldInfo);
// ret - return what's on top of
eval.stack
getterIL.Emit(OpCodes.Ret);
fieldGetters.Add(key, getter);
}
}
return fieldGetters[key];
}
public static
object GetFieldValue(
object
instance, string fieldName) {
return GetFieldGetterMethod(instance,
fieldName)
.Invoke(null, new[] {
instance });
}
private static readonly Dictionary<string, DynamicMethod>
fieldGetters = new Dictionary<string, DynamicMethod>();
private static readonly object fsync
= new object();
}
//
PropertySetter.cs
public
class PropertySetter
{
private static DynamicMethod GetPropertySetterMethod(
object instance, string
propertyName) {
var t = instance.GetType();
var key = (t.FullName + "_" + propertyName).Replace(".", "_");
if
(!propertySetter.ContainsKey(key)) {
lock (fsync) {
var
propertyInfo = t.GetProperty(propertyName,
BindingFlags.Instance |
BindingFlags.Public);
if (propertyInfo == null)
throw new ArgumentException("There is no publicly " +
"accessible " + propertyName +
" property found in " +
t.FullName + ".");
if (!propertyInfo.CanWrite)
throw new ArgumentException("The property " +
propertyName +
" has no publicly accessible setter.");
var setter = new DynamicMethod("__set_property_" + key, null,
new[] { t, propertyInfo.PropertyType },
typeof(PropertySetter));
var setterIL =
setter.GetILGenerator();
//
ldarg.0 - load argument 1 to eval.stack.0
setterIL.Emit(OpCodes.Ldarg_0);
//
ldarg.1 - load argument 2 to eval.stack.1
setterIL.Emit(OpCodes.Ldarg_1);
//
callvirt p - call set_* method to set the
// value from eval.stack.1
setterIL.Emit(OpCodes.Callvirt,
propertyInfo.GetSetMethod());
//
ret - exit the method
setterIL.Emit(OpCodes.Ret);
propertySetter.Add(key, setter);
}
}
return propertySetter[key];
}
public static void SetPropertyValue(
object
instance, string propertyName, object value) {
GetPropertySetterMethod(instance, propertyName)
.Invoke(null, new[] {
instance, value });
}
private static readonly Dictionary<string, DynamicMethod>
propertySetter = new Dictionary<string, DynamicMethod>();
private static readonly object fsync
= new object();
}
//
PropertyGetter.cs
public
class PropertyGetter
{
private static DynamicMethod GetPropertyGetterMethod(
object instance, string
propertyName) {
var t = instance.GetType();
var key = (t.FullName + "_" + propertyName).Replace(".", "_");
if
(!propertyGetters.ContainsKey(key)) {
lock (fsync) {
var propertyInfo =
t.GetProperty(propertyName,
BindingFlags.Instance |
BindingFlags.Public);
if (propertyInfo == null)
throw new ArgumentException("There is no publicly " +
"accessible " + propertyName +
" property found in " +
t.FullName + ".");
if (!propertyInfo.CanRead)
throw new ArgumentException("The property " + propertyName
+ " has no publicly accessible getter.");
var getter = new DynamicMethod("__get_property_" + key,
propertyInfo.PropertyType, new[] { t },
typeof(PropertyGetter));
var getterIL =
getter.GetILGenerator();
// ldarg.0 - load
argument 0 to eval.stack.0
getterIL.Emit(OpCodes.Ldarg_0);
// callvirt m - a
call to get_* method to get
// the property value
getterIL.Emit(OpCodes.Callvirt,
propertyInfo.GetGetMethod());
// ret - return the result of get_*
call
getterIL.Emit(OpCodes.Ret);
propertyGetters.Add(key, getter);
}
}
return propertyGetters[key];
}
public static object SetPropertyValue(
object
instance, string propertyName) {
return GetPropertyGetterMethod(instance, propertyName)
.Invoke(null, new[] {
instance });
}
private static readonly Dictionary<string, DynamicMethod>
propertyGetters = new Dictionary<string, DynamicMethod>();
private static readonly object fsync
= new object();
}
I hope this still fall under the category of assembly language
(even though CIL is not exactly assembly language)
See you again next time.