samedi 5 décembre 2020

Why does this linq with reflection statement beat my compiled expression tree?

Having been inspired by this blogpost I set out to refactor the following Linq query using compiled expression trees:

var result = dummies.Select(y =>
                   y.GetType().GetProperties()
                   .Where(x => x.GetMethod.IsPublic)
                   .Where(x => fields.Contains(x.Name, StringComparer.OrdinalIgnoreCase))
                   .ToDictionary(x => x.Name, x => x.GetValue(y)))
                   .Where(x => x.Any());

The code is intended to extract a set of values of specified properties, returning a dictionary for each element. In my first encounter with writing my own expression trees I've come up with this solution to generate the property calls:

        foreach (string propName in Properties)
        {
            var prop = typeof(DummyType).GetProperty(propName);

            if (prop != null)
            {
                props.Add(prop);
            }
        }

        var accessors = new List<Tuple<string, Func<DummyType, object>>>();

        foreach (var prop in props)
        {
            var instance = Expression.Parameter(typeof(DummyType));
            var call = Expression.Property(instance, prop);
            var expr = Expression.Lambda<Func<DummyType, object>>(call, instance).Compile();
            accessors.Add(Tuple.Create(prop.Name, expr));
        }

For each DummyType element the calls in accessors will be iterated This implementation is unable to deal with properties returning value types, though I was able to solve that issue using MakeGenericType combined with a DynamicInvoke call but because it's documented as "late-bound" I've discarded it to avoid it distorting the performance.

The results are surprising with the Linq query beating my expression trees hands down even though it boxes value types for me and GetProperties is called for each element, whereas the linq expression property accessors are generated before collecting the values from the collection of dummytypes.

|   Method |         Mean |      Error |     StdDev |  Ratio | RatioSD |
|--------- |-------------:|-----------:|-----------:|-------:|--------:|
|     Linq |     73.09 ns |   0.878 ns |   0.778 ns |   1.00 |    0.00 |
| ExprTree | 16,293.69 ns | 184.834 ns | 172.894 ns | 222.83 |    3.96 |

The benchmarks are generated using benchmark.net.

  1. Why is the expression tree approach significantly slower?
  2. Would it be fair to assume the expression tree solution is faster?
  3. Bonus: What effect does using the MakeGenericType solution have on performance in this context?




Aucun commentaire:

Enregistrer un commentaire