Field getter/setter with expression tree in base class
Asked Answered
G

1

8

Following the examples on this post and its follow-up question, I am trying to create field getters / setters using compiled expressions.

The getter works just great, but I am stuck the setter, as I need the setter to assign any type of fields.

Here my setter-action builder:

public static Action<T1, T2> GetFieldSetter<T1, T2>(this FieldInfo fieldInfo) {
  if (typeof(T1) != fieldInfo.DeclaringType && !typeof(T1).IsSubclassOf(fieldInfo.DeclaringType)) {
    throw new ArgumentException();
  }
  ParameterExpression targetExp = Expression.Parameter(typeof(T1), "target");
  ParameterExpression valueExp = Expression.Parameter(typeof(T2), "value");
  //
  // Expression.Property can be used here as well
  MemberExpression fieldExp = Expression.Field(targetExp, fieldInfo);
  BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp);
  //
  return Expression.Lambda<Action<T1, T2>> (assignExp, targetExp, valueExp).Compile();
}

Now, I store the generic setters into a cache list (because of course, building the setter each time is a performance killer), where I cast them as simple "objects":

 // initialization of the setters dictionary
 Dictionary<string, object> setters = new Dictionary(string, object)();
 Dictionary<string, FieldInfo> fldInfos = new Dictionary(string, FieldInfo)();
 FieldInfo f = this.GetType().GetField("my_int_field");
 setters.Add(f.Name, GetFieldSetter<object, int>(f); 
 fldInfos.Add(f.Name, f); 
 //
 f = this.GetType().GetField("my_string_field");
 setters.Add(f.Name, GetFieldSetter<object, string>(f); 
 fldInfos.Add(f.Name, f); 

Now I try to set a field value like this:

 void setFieldValue(string fieldName, object value) {
      var setterAction = setters[fieldName];
      // TODO: now the problem => how do I invoke "setterAction" with 
      // object and fldInfos[fieldName] as parameters...?
 }

I could simply call a generic method and cast each time, but I am worried about the performance overhead... Any suggestions?

-- EDITED ANSWER Based on Mr Anderson's answer, I created a small test program which compares directly setting the value, cached reflection (where FieldInfo's are cached) and the cached multi-type code. I use object inheritance with up to 3 level of inheritance (ObjectC : ObjectB : ObjectA).

Full code is of the example can be found here.

Single iteration of the test gives following output:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       0.0036 ms
  Set reflection:   2.319 ms
  Set ref.Emit:     1.8186 ms
  Set Accessor:     4.3622 ms

-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       0.0004 ms
  Set reflection:   0.1179 ms
  Set ref.Emit:     1.2197 ms
  Set Accessor:     2.8819 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       0.0024 ms
  Set reflection:   0.1106 ms
  Set ref.Emit:     1.1577 ms
  Set Accessor:     2.9451 ms

Of course, this simply shows the cost of creating the objects - this allows us to measure the offset of creating the cached versions of Reflection and Expressions.

Next, let's run 1.000.000 times:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       33.2744 ms
  Set reflection:   1259.9551 ms
  Set ref.Emit:     531.0168 ms
  Set Accessor:     505.5682 ms

-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       38.7921 ms
  Set reflection:   2584.2972 ms
  Set ref.Emit:     971.773 ms
  Set Accessor:     901.7656 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       40.3942 ms
  Set reflection:   3796.3436 ms
  Set ref.Emit:     1510.1819 ms
  Set Accessor:     1469.4459 ms

For the sake of completeness: I removed the call to the "set" method to highlight the cost of getting the setter (FieldInfo for the reflection method, Action<object, object> for the expression case). Here the results:

-------------------------
---      OBJECT A     ---
-------------------------
  Set direct:       3.6849 ms
  Set reflection:   44.5447 ms
  Set ref.Emit:     47.1925 ms
  Set Accessor:     49.2954 ms


-------------------------
---      OBJECT B     ---
-------------------------
  Set direct:       4.1016 ms
  Set reflection:   76.6444 ms
  Set ref.Emit:     79.4697 ms
  Set Accessor:     83.3695 ms

-------------------------
---      OBJECT C     ---
-------------------------
  Set direct:       4.2907 ms
  Set reflection:   128.5679 ms
  Set ref.Emit:     126.6639 ms
  Set Accessor:     132.5919 ms

NOTE: time increase here is not due to the fact that access times are slower for larger dictionaries (as they have O(1) access times), but due to the fact that the number of times we access it is increased (4 times per iteration for ObjectA, 8 for ObjectB, 12 for ObjectC)... As one sees, only the creation offset makes a difference here (which is to be expected).

Bottom line: we did improve performance by a factor of 2 or more, but we're still far away from the direct field set's performance... Retrieving the correct setter in the list represents a good 10% of the time.

I'll try with expression trees in place of Reflection.Emit to see if we can further reduce the gap... Any comment is more than welcome.

EDIT 2 I added results using the approach using a generic "Accessor" class as suggested by Eli Arbel on this post.

Gilding answered 22/7, 2016 at 13:7 Comment(3)
"Worried about the performance" doesn't quite cut it. Test it, see if it performs well enough, and decide based on that. I don't see any reason why using a generic method would be worse than your current approach.Eudoxia
I used to use this approach (expressions), then I discovered System.Reflection.Emit.DyanamicMethod is much more straightforward.Giantess
I think the Dynamic runtime caches also this sort of stuff. I don't think it would perform that much slower.Florous
G
1

If you want this to support operations on multiple types, your function cache should be indexed by Type AND field name (string), and the functions should be lazily created. Try this:

private static Dictionary<Type, Dictionary<string, Action<object, object>>> _typeMapper = new Dictionary<Type, Dictionary<string, Action<object, object>>>();

public static void Set(object obj, string fieldName, object newValue)
{
    if (obj == null)
    {
        throw new ArgumentNullException("obj");
    }
    Type type = obj.GetType();
    Dictionary<string, Action<object, object>> fieldMapper;
    Action<object, object> action;
    if (_typeMapper.TryGetValue(type, out fieldMapper))
    {
        // entry has been created for this type.
        if (!fieldMapper.TryGetValue(fieldName, out action))
        {
            // method has not been created yet, must build it.
            FieldInfo fld = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            if (fld == null)
            {
                throw new ArgumentException("No field " + fieldName);
            }
            action = buildSetter(fld);
            fieldMapper.Add(fieldName, action); // add it to method cache for future use.
        }
    }
    else
    {
        // -- ADDED CODE: forgot to create the new fieldMapper.....
        fieldMapper = new Dictionary<string, Action<object, object>>();

     // type has not been added yet, so we know method has not been built yet either.
        FieldInfo fld = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (fld == null)
        {
            throw new ArgumentException("No field " + fieldName);
        }
        action = buildSetter(fld);
        fieldMapper.Add(fieldName, action); // add it to method cache for future use.
        _typeMapper.Add(type, fieldMapper); // add it to type cache for future use.
    }
    action(obj, newValue); // invoke the method.
}
// this is my preferred setter-builder, feel free to use expressions instead.
private static Action<object, object> buildSetter(FieldInfo fld)
{
    DynamicMethod dyn = new DynamicMethod("set_" + fld, typeof(void), new[] { typeof(object), typeof(object) }, fld.DeclaringType);
    ILGenerator gen = dyn.GetILGenerator();
    gen.Emit(OpCodes.Ldarg_0);
    gen.Emit(OpCodes.Castclass, fld.DeclaringType);
    gen.Emit(OpCodes.Ldarg_1);
    if (fld.FieldType.IsClass)
    {
        gen.Emit(OpCodes.Castclass, fld.FieldType);
    }
    else
    {
        gen.Emit(OpCodes.Unbox_Any, fld.FieldType);
    }
    gen.Emit(OpCodes.Stfld, fld);
    gen.Emit(OpCodes.Ret);
    return (Action<object, object>)dyn.CreateDelegate(typeof(Action<object, object>));
}

Otherwise, if you need only do this with one type, your process becomes:

private static Dictionary<string, Action<MyType, object>> _mapper = new Dictionary<string, Action<MyType, object>>();

public static void Set(MyType obj, string fieldName, object newValue)
{
    if (obj == null)
    {
        throw new ArgumentNullException("obj");
    }
    Action<MyType, object> action;
    if (!_mapper.TryGetValue(fieldName, out action))
    {
        FieldInfo fld = typeof(MyType).GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        if (fld == null)
        {
            throw new ArgumentException("No field " + fieldName);
        }
        action = buildSetter(fld);
        _mapper.Add(fieldName, action);
    }
    action(obj, newValue); // invoke the method.
}

private static Action<MyType, object> buildSetter(FieldInfo fld)
{
    DynamicMethod dyn = new DynamicMethod("set_" + fld, typeof(void), new[] { typeof(MyType), typeof(object) }, typeof(MyType));
    ILGenerator gen = dyn.GetILGenerator();
    gen.Emit(OpCodes.Ldarg_0);
    gen.Emit(OpCodes.Ldarg_1);
    if (fld.FieldType.IsClass)
    {
        gen.Emit(OpCodes.Castclass, fld.FieldType);
    }
    else
    {
        gen.Emit(OpCodes.Unbox_Any, fld.FieldType);
    }
    gen.Emit(OpCodes.Stfld, fld);
    gen.Emit(OpCodes.Ret);
    return (Action<MyType, object>)dyn.CreateDelegate(typeof(Action<MyType, object>));
}
Giantess answered 22/7, 2016 at 14:53 Comment(1)
Great answer, thanks. I'll write a little performance test programm to check performances of both and post it here.Gilding

© 2022 - 2024 — McMap. All rights reserved.