One of the recurring themes on this blog is the idea of making the computer work harder, not the user or the developer. I discovered recently that there was an easier and faster reflection technique than the one I was using - the technique that I’d been using for most of a decade!

Here’s an example of the slow way that anyone getting started with .NET Reflection would naturally write:

public string ReadNameNaive()
{
    var property = typeof(Container).GetProperty(
        "Name",
        BindingFlags.Instance
        | BindingFlags.GetProperty
        | BindingFlags.Public 
        | BindingFlags.NonPublic);
    return (string)property.GetValue(_container);
}

This works - but it’s hardly very performant. Let’s look at how to make it better.

The value for the property variable doesn’t change from call to call, so it makes sense to cache the value once:

private static readonly PropertyInfo _property =
    typeof(Container).GetProperty(
        "Name",
        BindingFlags.Instance
        | BindingFlags.GetProperty
        | BindingFlags.Public
        | BindingFlags.NonPublic);

public string ReadNameCached()
{
    return (string)_property.GetValue(_container);
}

Comparing the performance of the two (using BenchmarkDotNet), we get the following:

MethodMeanErrorStdDev
ReadNameNaive268.629 ns5.4141 ns6.4450 ns
ReadNameCached186.047 ns3.6928 ns4.6702 ns

The cached version is substantially faster - but we can do even better.

We can use the .NET Expressions framework, part of LINQ, to dynamically compile a lambda expression to do the read. The code is a little complex - but it just takes a bit of trial and error to get right:

private static readonly Func<Container, string> _nameReader
    = GetNameReader();

private static Func<Container, string> GetNameReader()
{
    var property = typeof(Container).GetProperty(
        "Name",
        BindingFlags.Instance
        | BindingFlags.GetProperty
        | BindingFlags.Public
        | BindingFlags.NonPublic);
    var propertyReader = property.GetMethod;
    var containerParameter = Expression.Parameter(typeof(Container));
    var readPropertyCall = Expression.Call(
        containerParameter, propertyReader);
    var castToString = Expression.Convert(readPropertyCall, typeof(string));
    var lambda = Expression.Lambda<Func<Container, string>>(
        castToString, containerParameter);
    return lambda.Compile();
}

public string ReadNameExpression()
{
    return _nameReader(_container);
}

It’s worth noting that there is a substantial up-front initialization cost caused by the compilation, but it soon pays off. I’ve written code like this many times, and the performance is spectacularly better than the other options - 20x faster:

MethodMeanErrorStdDev
ReadNameNaive268.629 ns5.4141 ns6.4450 ns
ReadNameCached186.047 ns3.6928 ns4.6702 ns
ReadNameExpression9.120 ns0.2378 ns0.2224 ns

What I learnt recently was that there’s a much easier way to achieve the same result:

private static readonly Func<Container, string> _nameReader
    = GetNameReader();

private static Func<Container, string> GetNameReader()
{
    var property = typeof(Container).GetProperty(
        "Name",
        BindingFlags.Instance
        | BindingFlags.GetProperty
        | BindingFlags.Public
        | BindingFlags.NonPublic);
    var propertyReader = property.GetMethod;
    return (Func<Container, string>)
        propertyReader.CreateDelegate(typeof(Func<Container, string>));
}

public string ReadNameDelegate()
{
    return _nameReader(_container);
}

Very simple and elegant. Straightforward to write and easy to read. But how well does it work?

MethodMeanErrorStdDev
ReadNameNaive268.629 ns5.4141 ns6.4450 ns
ReadNameCached186.047 ns3.6928 ns4.6702 ns
ReadNameExpression9.120 ns0.2378 ns0.2224 ns
ReadNameDelegate2.460 ns0.1217 ns0.1138 ns

That’s better than 100x faster performance than the original and nearly 4x faster than the technique I thought was the fastest possible. Nice.

Updated Monday 15th October: Modified code examples to be more explicit.

Comments

blog comments powered by Disqus
Next Post
Introducing the Priority Queue  20 Oct 2018
Prior Post
Avoiding Magic Strings  06 Oct 2018
Related Posts
Simple Queues  17 Nov 2018
Enqueuing Values  10 Nov 2018
Dequeuing Values  03 Nov 2018
Designing the External API  27 Oct 2018
Introducing the Priority Queue  20 Oct 2018
Avoiding Magic Strings  06 Oct 2018
Capturing Validation Metadata  29 Sep 2018
Modelling Validation Metadata  22 Sep 2018
Validation Metadata  15 Sep 2018
Extending validation with warnings  08 Sep 2018
More csharp posts »
Archives
October 2018
2018