danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Comparing dynamic result extraction of a generic Task in C#

Have been working with some async in-process message routing lately and most recently request-and-response messaging pattern. There's a mediator that sits in between, and it only receives the result from the handler processing the requests as a Task. The actual implementation is a generic Task<> and the mediator needs to dynamically extract the value as an object. In doing so there are different options to extract it from Task.Result. We will look at implementation and benchmarks using: reflection, IL-code generation, compiled lambdas and extraction using dynamic.

Follow up post: Due to comment on this post there's now some updated benchmarks. See: "New interesting results for dynamic result extraction of a generic Task in C#"

Updated 2017-03-04: There was an issue in timing the Lambda cache. The Code has been corrected and the results in the post have been updated

Winner?

Remember. This is for a specific scenario and might not match your case. But in this case, the "Winner" is dynamic.

  1. dynamic
  2. IL-code
  3. Compiled lambdas
  4. Reflection

Benchmarks

Benchmarking is performed using the nice project: BenchmarkDotNet.

BenchmarkDotNet=v0.10.1, OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4790K CPU 4.00GHz, ProcessorCount=8
Frequency=3906248 Hz, Resolution=256.0001 ns, Timer=TSC
  [Host]     : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1586.0
  DefaultJob : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1586.0
                             Method | Count |           Mean |     StdDev |
----------------------------------- |------ |--------------- |----------- |
                    UsingReflection |     1 |    156.7207 ns |  0.1411 ns |
        UsingReflectionWithSameProp |     1 |    100.7052 ns |  0.2246 ns |
           UsingReflectionWithCache |     1 |    123.1107 ns |  0.0944 ns |
              UsingIlWithSameGetter |     1 |      4.9922 ns |  0.0075 ns |
                   UsingIlWithCache |     1 |     22.7416 ns |  0.0433 ns |
 UsingCompiledLambdasWithSameGetter |     1 |      5.5873 ns |  0.0414 ns |
      UsingCompiledLambdasWithCache |     1 |     23.8770 ns |  0.0656 ns |
                       UsingDynamic |     1 |      4.6833 ns |  0.0027 ns |
                    UsingReflection |    10 |  1,578.7332 ns |  2.9305 ns |
        UsingReflectionWithSameProp |    10 |  1,008.6202 ns |  0.6704 ns |
           UsingReflectionWithCache |    10 |  1,240.7958 ns |  0.6964 ns |
              UsingIlWithSameGetter |    10 |     55.3914 ns |  0.0489 ns |
                   UsingIlWithCache |    10 |    232.9545 ns |  0.1964 ns |
 UsingCompiledLambdasWithSameGetter |    10 |     59.2868 ns |  0.1321 ns |
      UsingCompiledLambdasWithCache |    10 |    247.6450 ns |  3.0892 ns |
                       UsingDynamic |    10 |     43.7722 ns |  0.0748 ns |
                    UsingReflection |   100 | 15,759.5362 ns | 29.5904 ns |
        UsingReflectionWithSameProp |   100 | 10,046.1404 ns |  7.1693 ns |
           UsingReflectionWithCache |   100 | 12,366.3188 ns | 25.4015 ns |
              UsingIlWithSameGetter |   100 |    510.4310 ns |  0.6951 ns |
                   UsingIlWithCache |   100 |  2,290.5035 ns |  2.9381 ns |
 UsingCompiledLambdasWithSameGetter |   100 |    554.2357 ns |  0.9671 ns |
      UsingCompiledLambdasWithCache |   100 |  2,374.0598 ns |  2.0419 ns |
                       UsingDynamic |   100 |    444.2639 ns |  0.4209 ns |

Complete benchmark sample

using System;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApplication6
{
  class Program
  {
    static void Main(string[] args)
    {
      BenchmarkRunner.Run<ExtractFromTask>();
    }
  }

  public class ExtractFromTask
  {
    private Task<Thing> _task;
    private Type _taskType;

    private PropertyInfo _prop;
    private readonly PropInfoCache _propCache = new PropInfoCache();

    private DynamicGetter _ilGetter;
    private readonly DynamicIlGetterCache _ilGetterCache
      = new DynamicIlGetterCache();

    private DynamicGetter _compiledLambdaGetter;
    private readonly DynamicCompiledLambdaGetterCache _compiledLambdaGetterCache
      = new DynamicCompiledLambdaGetterCache();

    [Params(1, 10, 100)]
    public int Count { get; set; }

    [Setup]
    public void Setup()
    {
      _task = Task.FromResult(new Thing("Test"));
      _taskType = _task.GetType();
      _prop = _taskType.GetProperty("Result");
      _ilGetter = DynamicGetterFactory.CreateUsingIl(_prop);
      _compiledLambdaGetter = DynamicGetterFactory.CreateUsingCompiledLambda(_prop);
    }

    [Benchmark]
    public void UsingReflection()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _task
          .GetType()
          .GetProperty("Result")
          .GetValue(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingReflectionWithSameProp()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _prop.GetValue(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingReflectionWithCache()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _propCache.GetFor(_taskType).GetValue(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingIlWithSameGetter()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _ilGetter(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingIlWithCache()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _ilGetterCache.GetFor(_taskType)(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingCompiledLambdasWithSameGetter()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _compiledLambdaGetter(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingCompiledLambdasWithCache()
    {
      for (var i = 0; i < Count; i++)
      {
        var thing = _compiledLambdaGetterCache.GetFor(_taskType)(_task) as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }

    [Benchmark]
    public void UsingDynamic()
    {
      for (var i = 0; i < Count; i++)
      {
        dynamic d = _task;
        var thing = d.Result as Thing;

        if (thing == null)
          throw new Exception("Extraction failed");
      }
    }
  }

  public class Thing
  {
    public string Value { get; }

    public Thing(string value)
    {
      Value = value;
    }
  }

  public class PropInfoCache
  {
    private readonly ConcurrentDictionary<Type, PropertyInfo> _state =
      new ConcurrentDictionary<Type, PropertyInfo>();

    public PropertyInfo GetFor(Type type) => _state.GetOrAdd(
      type, t => t.GetProperty("Result"));
  }

  public class DynamicIlGetterCache
  {
    private readonly ConcurrentDictionary<Type, DynamicGetter> _state
      = new ConcurrentDictionary<Type, DynamicGetter>();

    public DynamicGetter GetFor(Type type) => _state.GetOrAdd(
      type, t => DynamicGetterFactory.CreateUsingIl(t.GetProperty("Result")));
  }

  public class DynamicCompiledLambdaGetterCache
  {
    private readonly ConcurrentDictionary<Type, DynamicGetter> _state
      = new ConcurrentDictionary<Type, DynamicGetter>();

    public DynamicGetter GetFor(Type type) => _state.GetOrAdd(
      type, t => DynamicGetterFactory.CreateUsingCompiledLambda(t.GetProperty("Result")));
  }

  public delegate object DynamicGetter(object obj);

  public static class DynamicGetterFactory
  {
    private static readonly Type ObjectType = typeof(object);
    private static readonly Type IlGetterType = typeof(Func<object, object>);

    public static DynamicGetter CreateUsingIl(PropertyInfo p)
      => new DynamicGetter(CreateIlGetter(p));

    public static DynamicGetter CreateUsingCompiledLambda(PropertyInfo p)
      => new DynamicGetter(CreateLambdaGetter(p.DeclaringType, p));

    private static Func<object, object> CreateLambdaGetter(
      Type type,
      PropertyInfo property)
    {
      var objExpr = Expression.Parameter(ObjectType, "theItem");
      var castedObjExpr = Expression.Convert(objExpr, type);

      var p = Expression.Property(castedObjExpr, property);
      var castedProp = Expression.Convert(p, ObjectType);

      var lambda = Expression.Lambda<Func<object, object>>(castedProp, objExpr);

      return lambda.Compile();
    }

    private static Func<object, object> CreateIlGetter(PropertyInfo propertyInfo)
    {
      var propGetMethod = propertyInfo.GetGetMethod(true);
      if (propGetMethod == null)
        return null;

      var getter = CreateDynamicGetMethod(propertyInfo);
      var generator = getter.GetILGenerator();

      var x = generator.DeclareLocal(propertyInfo.DeclaringType);//Arg
      var y = generator.DeclareLocal(propertyInfo.PropertyType); //Prop val
      var z = generator.DeclareLocal(ObjectType); //Prop val as obj

      generator.Emit(OpCodes.Ldarg_0);
      generator.Emit(OpCodes.Castclass, propertyInfo.DeclaringType);
      generator.Emit(OpCodes.Stloc, x);

      generator.Emit(OpCodes.Ldloc, x);
      generator.EmitCall(OpCodes.Callvirt, propGetMethod, null);
      generator.Emit(OpCodes.Stloc, y);

      generator.Emit(OpCodes.Ldloc, y);

      if (!propertyInfo.PropertyType.IsClass)
      {
        generator.Emit(OpCodes.Box, propertyInfo.PropertyType);
        generator.Emit(OpCodes.Stloc, z);
        generator.Emit(OpCodes.Ldloc, z);
      }

      generator.Emit(OpCodes.Ret);

      return (Func<object, object>)getter.CreateDelegate(IlGetterType);
    }

    private static DynamicMethod CreateDynamicGetMethod(PropertyInfo propertyInfo)
    {
      var args = new[] { ObjectType };
      var name = $"_{propertyInfo.DeclaringType.Name}_Get{propertyInfo.Name}_";
      var returnType = ObjectType;

      return !propertyInfo.DeclaringType.IsInterface
             ? new DynamicMethod(
               name,
               returnType,
               args,
               propertyInfo.DeclaringType,
               true)
             : new DynamicMethod(
               name,
               returnType,
               args,
               propertyInfo.Module,
               true);
    }
  }
}

That's all. Remember. Test for your case. Just because using dynamic in the scenario above was fastest, doesn't mean it is for you. So be sure to benchmark and evaluate for your scenario. Some background info can be found here.

Cheers,

//Daniel

View Comments