0

I have small piece of code responsible for dynamic extraction of properties values from objects instances through reflection:

public static object ExtractValue(object source, string property)
{
    var props = property.Split('.');
    var type = source.GetType();
    var arg = Expression.Parameter(type, "x");
    Expression expr = arg;
    foreach (var prop in props)
    {
        var pi = type.GetProperty(prop);
        if (pi == null)
            throw new ArgumentException(string.Format("Field {0} not found.", prop));
        expr = Expression.Property(expr, pi);
        type = pi.PropertyType;
    }
    var delegateType = typeof(Func<,>).MakeGenericType(source.GetType(), type);
    var lambda = Expression.Lambda(delegateType, expr, arg);

    var compiledLambda = lambda.Compile();
    var value = compiledLambda.DynamicInvoke(source);
    return value;
}

It can extract values of nested properties, like: ExtractValue(instance, "PropA.PropB.PropC").

Despite the fact I like this method and its implementation, when, say, PropB is null, DynamicInvoke() just throws NullReferenceException (wrapped by TargetInvocationException). Because I needed to know which exact property is null is such case, I modified its body a bit (standard step-by-step extraction chain):

public static object ExtractValue(object source, string property)
{
    var props = property.Split('.');
    for (var i = 0; i < props.Length; i++)
    {
        var type = source.GetType();
        var prop = props[i];
        var pi = type.GetProperty(prop);
        if (pi == null)
            throw new ArgumentException(string.Format("Field {0} not found.", prop));
        source = pi.GetValue(source, null);
        if (source == null && i < props.Length - 1)
            throw new ArgumentNullException(pi.Name, "Extraction interrupted.");
    }
    return source;
}

Now it looks a bit worse (I like lambdas) but behaves much better, not only because it gives more meaningful information of what has failed, but also because this version is about 66 times faster than the first one (coarse test below):

var model = new ModelA
{
    PropB = new ModelB {PropC = new ModelC {PropD = new ModelD {PropE = new ModelE {PropF = "hey"}}}}
};
const int times = 1000000;

var start = DateTime.Now;
for (var i = 0; i < times; i++)
    ExtractValueFirst(model, "PropB.PropC.PropD.PropE.PropF");
var ticks_first = (DateTime.Now - start).Ticks;
Console.WriteLine(":: first  - {0} iters tooks {1} ticks", times, ticks_first);

start = DateTime.Now;
for (var i = 0; i < times; i++)
    ExtractValueSecond(model, "PropB.PropC.PropD.PropE.PropF");
var ticks_second= (DateTime.Now - start).Ticks;
Console.WriteLine(":: second - {0} iters tooks {1} ticks", times, ticks_second);

Console.WriteLine("ticks_first/ticks_second: {0}", (float)ticks_first / ticks_second);
Console.ReadLine();

enter image description here

How can this code be optimized in .NET to perform even faster (caching, direct IL maybe, etc)?

jwaliszko
  • 16,942
  • 22
  • 92
  • 158
  • I once wrote a class that is almost doing the same. You will speedup significantly when you cache the compiled delegates, as this procedure takes the longest amount of time. You dan do this by using a Tuple as Key. Where type is the objectType and the string is the expression – CSharpie May 28 '14 at 21:56
  • Also you might wanna fumble aaround with marc gravells http://www.nuget.org/packages/FastMember – CSharpie May 29 '14 at 15:34

2 Answers2

3

You can increase performance significantly by caching the compiled delegates:

static readonly ConcurrentDictionary<Tuple<Type,string>,Delegate> _delegateCache = new ConcurrentDictionary<Tuple<Type,string>,Delegate>();

public static object ExtractValue(object source, string expression)
{
    Type type = source.GetType();
    Delegate del =  _delegateCache.GetOrAdd(new Tuple<Type,string>(type,expression),key => _getCompiledDelegate(key.Item1,key.Item2));
    return del.DynamicInvoke(source);
}

// if you want to acces static aswell...
public static object ExtractStaticValue(Type type, string expression)
{
    Delegate del =  _delegateCache.GetOrAdd(new Tuple<Type,string>(type,expression),key => _getCompiledDelegate(key.Item1,key.Item2));
    return del.DynamicInvoke(null);
}

private static Delegate _getCompiledDelegate(Type type, string expression)
{
    var arg = Expression.Parameter(type, "x");
    Expression expr = arg;
    foreach (var prop in property.Split('.'))
    {
        var pi = type.GetProperty(prop);
        if (pi == null) throw new ArgumentException(string.Format("Field {0} not found.", prop));
        expr = Expression.Property(expr, pi);
        type = pi.PropertyType;
    }
    var delegateType = typeof(Func<,>).MakeGenericType(source.GetType(), type);
    var lambda = Expression.Lambda(delegateType, expr, arg);

    return lambda.Compile();
}
CSharpie
  • 9,195
  • 4
  • 44
  • 71
  • +1 Thanks for the tip. Based on that I've tested various versions of this logic and posted the results here. Regards. – jwaliszko May 29 '14 at 15:20
0

I've done some execution time measurements, which are presented below:

private static Func<object, object> _cachedFunc;
private static Delegate _cachedDel;

static void Main(string[] args)
{
    var model = new ModelA
    {
        PropB = new ModelB {PropC = new ModelC {PropD = new ModelD {PropE = new ModelE {PropF = "hey"}}}}
    };            
    const string property = "PropB.PropC.PropD.PropE.PropF";
    var watch = new Stopwatch();

    var t1 = MeasureTime(watch, () => ExtractValueDelegate(model, property), "compiled delegate dynamic invoke");
    var t2 = MeasureTime(watch, () => ExtractValueCachedDelegate(model, property), "compiled delegate dynamic invoke / cached");
    var t3 = MeasureTime(watch, () => ExtractValueFunc(model, property), "compiled func invoke");
    var t4 = MeasureTime(watch, () => ExtractValueCachedFunc(model, property), "compiled func invoke / cached");
    var t5 = MeasureTime(watch, () => ExtractValueStepByStep(model, property), "step-by-step reflection");
    var t6 = MeasureTime(watch, () => ExtractValueStandard(model), "standard access (model.prop.prop...)");

    Console.ReadLine();
}

public static long MeasureTime<T>(Stopwatch sw, Func<T> funcToMeasure, string funcName)
{
    const int times = 100000;
    sw.Reset();

    sw.Start();
    for (var i = 0; i < times; i++)
        funcToMeasure();
    sw.Stop();

    Console.WriteLine(":: {0, -45}  - {1} iters tooks {2, 10} ticks", funcName, times, sw.ElapsedTicks);
    return sw.ElapsedTicks;
}

public static object ExtractValueDelegate(object source, string property)
{        
    var ptr = GetCompiledDelegate(source.GetType(), property);
    return ptr.DynamicInvoke(source);            
}

public static object ExtractValueCachedDelegate(object source, string property)
{        
    var ptr = _cachedDel ?? (_cachedDel = GetCompiledDelegate(source.GetType(), property));
    return ptr.DynamicInvoke(source);
}

public static object ExtractValueFunc(object source, string property)
{        
    var ptr = GetCompiledFunc(source.GetType(), property);
    return ptr(source); //return ptr.Invoke(source);
}        

public static object ExtractValueCachedFunc(object source, string property)
{        
    var ptr = _cachedFunc ?? (_cachedFunc = GetCompiledFunc(source.GetType(), property));
    return ptr(source); //return ptr.Invoke(source);
}

public static object ExtractValueStepByStep(object source, string property)
{
    var props = property.Split('.');
    for (var i = 0; i < props.Length; i++)
    {
        var type = source.GetType();
        var prop = props[i];
        var pi = type.GetProperty(prop);
        if (pi == null)
            throw new ArgumentException(string.Format("Field {0} not found.", prop));
        source = pi.GetValue(source, null);
        if (source == null && i < props.Length - 1)
            throw new ArgumentNullException(pi.Name, "Extraction interrupted.");
    }
    return source;
}

public static object ExtractValueStandard(ModelA source)
{
    return source.PropB.PropC.PropD.PropE.PropF;
}

private static Func<object, object> GetCompiledFunc(Type type, string property)
{        
    var arg = Expression.Parameter(typeof(object), "x");
    Expression expr = Expression.Convert(arg, type);
    var propType = type;
    foreach (var prop in property.Split('.'))
    {
        var pi = propType.GetProperty(prop);
        if (pi == null) throw new ArgumentException(string.Format("Field {0} not found.", prop));
        expr = Expression.Property(expr, pi);
        propType = pi.PropertyType;
    }
    expr = Expression.Convert(expr, typeof(object));
    var lambda = Expression.Lambda<Func<object, object>>(expr, arg);
    return lambda.Compile();
}

private static Delegate GetCompiledDelegate(Type type, string property)
{        
    var arg = Expression.Parameter(type, "x");
    Expression expr = arg;
    var propType = type;
    foreach (var prop in property.Split('.'))
    {
        var pi = propType.GetProperty(prop);
        if (pi == null) throw new ArgumentException(string.Format("Field {0} not found.", prop));
        expr = Expression.Property(expr, pi);
        propType = pi.PropertyType;
    }
    var delegateType = typeof(Func<,>).MakeGenericType(type, propType);
    var lambda = Expression.Lambda(delegateType, expr, arg);
    return lambda.Compile();
}

enter image description here

Btw: As you can see I've omitted storing compiled lambdas inside dictionary (like in the answer qiven by CSharpie), because dictionary lookup is time consuming when you compare it to compiled lambdas execution time.

jwaliszko
  • 16,942
  • 22
  • 92
  • 158