Using Generated Methods Instead of Reflection

Introduction

It is a common thing to say that reflection is slow.You will find tons of posts saying this, and I generally tend to agree to them, although in most cases we generally don’t need to care that much – in fact, so many libraries and frameworks that we rely on daily depend on it!

When we talk about reflection, what is it that we talk about, normally? I’d say:

  • Getting an instance’s type, or base type, or implemented interfaces
  • Getting a list of properties, or fields
  • Setting values for properties, or fields
  • Finding methods
  • Invoking methods

Some things we can do to speed up the process include caching the PropertyInfo and FieldInfo objects, so that there is no need to retrieve them again and again from the declaring Type, but when it comes to getting and setting actual values, no caching can help here.

One thing that we can do, though, is generate code for actually accessing the properties/fields for us, in a strongly typed manner, without using reflection. This incurs in a startup cost, because of the overhead associated with the code generation, but after that it becomes much faster than the reflection-based code. Without more ado, let’s jump to the code.

First, some base class with just a single method for representing a simple setter operation:

public abstract class Setter {    

public abstract void Set(object obj, string propertyName, object value);

}

Simple, right? We take a target object (obj), a property (propertyName) and some value, and we’re good.

Reflection

The simplest, reflection-based implementation could look like this (ReflectionSetter):

public sealed class ReflectionSetter : Setter

{

     public override void Set(object obj, string propertyName, object value)

     {

         ArgumentNullException.ThrowIfNull(obj);

         ArgumentNullException.ThrowIfNull(propertyName);

         var property = obj.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty);

         if (property != null)

         {

             property.SetValue(obj, value, null);

         }

         else

         {

             throw new InvalidOperationException("Property not found.");

         }

     }

}

As you can see, nothing special here, the code does what you might expect: only public, instance properties with both a setter and a getter are looked for, and if not found, an exception is thrown.

Reflection With Caching

Another, slightly more elaborared version, which caches the PropertyInfo objects, but which requires that all types be initialized prior to being used (CachedReflectionSetter):

public sealed class CachedReflectionSetter : Setter

{

     private readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _properties = new Dictionary<Type, Dictionary<string, PropertyInfo>>();

     public void Initialize(Type type)

     {

         ArgumentNullException.ThrowIfNull(type);

         this._properties[type] = new Dictionary<string, PropertyInfo>();

         foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty))

         {

             this._properties[type][prop.Name] = prop;

         }

     }

     public override void Set(object obj, string propertyName, object value)

     {

         ArgumentNullException.ThrowIfNull(obj);

         ArgumentNullException.ThrowIfNull(propertyName);

         var property = this.GetPropertyFor(obj.GetType(), propertyName);

         if (property != null)

         {

             property.SetValue(obj, value, null);

         }

         else

         {

             throw new InvalidOperationException("Property not found.");

         }

     }

     private PropertyInfo? GetPropertyFor(Type type, string propertyName)

     {

         if (this._properties.TryGetValue(type, out var properties))

         {

             if (properties.TryGetValue(propertyName, out var prop))

             {

                 return prop;

             }

         }

         return null;

     }

}

The only difference between the two is that this one does not retrieve the PropertyInfo objects on the fly from the passed instance, instead it does so from a dictionary. But one must not forget to call its Initialize method with all the Types that we want to use it with, otherwise, it will not know its properties.

Compiled Lambda

Now things get a little more complext, as we want to get away from reflection – at least at the runtime level, when we are setting the value for the property, which is something that can potentially happen a lot of times during the lifefime of our application. So, enter the third iteration (CompiledSetter):

public sealed class CompiledSetter : Setter

{

     private readonly Dictionary<Type, Dictionary<string, Delegate>> _properties = new Dictionary<Type, Dictionary<string, Delegate>>();

     public void Initialize(Type type)

     {

         ArgumentNullException.ThrowIfNull(type);

         this._properties[type] = new Dictionary<string, Delegate>();

         foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty))

         {

            this.GenerateSetterFor(type, prop);

         }

     }

     public override void Set(object obj, string propertyName, object value)

     {

         ArgumentNullException.ThrowIfNull(obj);

         ArgumentNullException.ThrowIfNull(propertyName);

         var action = this.GetActionFor(obj.GetType(), propertyName);

         if (action is Action<object, object> act)

         {

                act(obj, value);

         }

         else

         {

             throw new InvalidOperationException("Property not found.");

         }

     }

     private void GenerateSetterFor(Type type, PropertyInfo property)

     {

         var propertyName = property.Name;

         var propertyType = property.PropertyType;

         var parmExpression = Expression.Parameter(typeof(object), "it");

         var castExpression = Expression.Convert(parmExpression, type);

         var propertyExpression = Expression.Property(castExpression, propertyName);

         var valueExpression = Expression.Parameter(typeof(object), propertyName);

         var operationExpression = Expression.Assign(propertyExpression, Expression.Convert(valueExpression, propertyType));

         var lambdaExpression = Expression.Lambda(typeof(Action<,>).MakeGenericType(typeof(object), typeof(object)), operationExpression, parmExpression, valueExpression);

         var action = lambdaExpression.Compile();

         this._properties[type][propertyName] = action;

     }

     private Delegate? GetActionFor(Type type, string propertyName)

     {

         if (this._properties.TryGetValue(type, out var properties))

         {

             if (properties.TryGetValue(propertyName, out var action))

             {

                 return action;

             }

         }

         return null;

     }

}

The approach here is to generate an expression for a lambda that accesses the property that we want to access and then compile it. The problem is that it would normally produce something like this:

Action<MyEntity> (it) => it.MyProperty = MyValue;

Which is not what we want, essentially, because with this we cannot call it dynamically – we do not know, at runtime, the MyEntity type or the type of MyProperty! What we want, and always have, is object, so, instead, we want to go with something along these lines:

Action<object, object> (it, prop) => ((MyEntity) it).MyProperty = (MyPropertyType)prop;

This way, we can always call the generated lambda with the target object and we

And it works like a charm! Winking smile

Performance Tests

I did a few tests with BenchmarkDotNet:

public class Entity

{

    public int Id { get; set; }

    public string Name { get; set; }

}

public class Test

{

     static CompiledSetter compiledSetter = new CompiledSetter();

     static ReflectionSetter reflectionSetter = new ReflectionSetter();

     static CachedReflectionSetter cachedReflectionSetter = new CachedReflectionSetter();

     static Entity[]? entities = null;

     [GlobalSetup]

     public static void Setup()

     {

         compiledSetter.Initialize(typeof(Entity));

         cachedReflectionSetter.Initialize(typeof(Entity));

         entities = Enumerable.Range(0, 200).Select(x => new Entity()).ToArray();

     }

     private void Common(Setter setter)

     {

         for (var i = 0; i < entities?.Length; i++)

         {

             setter.Set(entities[i], nameof(Entity.Id), i);

             setter.Set(entities[i], nameof(Entity.Name), i.ToString());

         }

     }

     [Benchmark]

     public void TestCompiled() => Common(compiledSetter);

     [Benchmark]

     public void TestReflection() => Common(reflectionSetter);

     [Benchmark]

     public void TestCachedReflection() => Common(cachedReflectionSetter);

}

Essentially, the tests generated a few (200) entities with two properties, the setters that required initialization were initialized, and then the properties were set in a loop.

The results I got were:

results

As you can see, the compiled version (CompiledSetter) outperformed the other two by a great margin. There’s also a small benefit in caching the PropertyInfo, as CachedReflectionSetter version arrived second on the results.

Conclusion

It is clear that reflection is indeed slow and a few things can definitely be done about it. I showed here setting property values, but the same logic can be done for getting them, and I’m happy to provide the code to anyone interested. If you wish to discuss any aspects of this, or have any questions, just give me a shout. As always, hope you find this useful!

                             

14 Comments

  • You can do better with Roslyn Source Code Generator.
    See https://github.com/ignatandrei/RSCG_Examples - see metadata from object example

  • While this is interesting anecdotally, constructing expressions this way for anything other than trivial expressions is clearly going to take the average dev a lot of time, given the high complexity and steep learning curve involved here.

    As a point of comparison, I'd be interested to see how a string dynamically compiled into code using the `CSharpCodeProvider` class would fare in your benchark. Starting with a C# string and merging some type and field names in will be a lot more accessible to the majority of devs I would think.

    Just a thought about an alternative way to avoid reflection...

  • @Ignat: yes, I am aware of Roslyn. I don't think it is easier or more practical than compiling a lambda expression, though, but I will add a setter based on it to the code sample on https://github.com/rjperes/PerformanceApp. Thanks!

  • @RichardH: sure, this was just about an alternative way to set properties while avoiding reflection, that's all.

  • Under the hood there’s still reflection going on…you’re just creating a delegate expression and caching that.

  • FastMember is a lot faster. 6 times or more.

  • @Andre: as I said, it is not needed at runtime, only at the creation.

  • @Il busca: a lot faster than what?

  • using your code with a datareader is not as fast as using FastMember or Dapper. your code is 6 time slower and with a lot of memory allocations.

  • @Il busca: did I mention anywhere "reducing allocations" as the purpose of my code? Or DataReader? This code merely sets properties on POCOs, that's what it's good at. For the sake of completeness, I tested with FastMember, which only allows creating proxies for instances, not types - if it did, we could store the proxies, which would greatly improve the performance of my example - and it scored poorer than CompiledSetter. The full code is in GitHub.

  • is this code available to check it out? thanks

  • @Pedro: yes, https://github.com/rjperes/PerformanceApp

  • What is the namespace of the Setter class in the Lambda variant?

  • @Andrii: please check the code on https://github.com/rjperes/PerformanceApp.

Add a Comment

As it will appear on the website

Not displayed

Your website