Search code examples
c#.net-corebenchmarkingexpression-trees

Why is creating and using an Expression faster than direct access?


I am currently implementing some dynamic filtering/sorting and thought it was a good idea to do a benchmark to see how things are looking.

First, here's the method creating an expression acting as a "getter":

public static Expression<Func<TEntity, object>> GetPropertyGetter(string propertyName, bool useCache = false)
{
    if (useCache && _propertyGetters.ContainsKey(propertyName))
        return _propertyGetters[propertyName];

    var entityType = typeof(TEntity);
    var property = entityType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
    if (property == null)
        throw new Exception($"Property {propertyName} was not found in entity {entityType.Name}");

    var param = Expression.Parameter(typeof(TEntity));
    var prop = Expression.Property(param, propertyName);
    var convertedProp = Expression.Convert(prop, typeof(object));
    var expr = Expression.Lambda<Func<TEntity, object>>(convertedProp, param);

    if (useCache)
    {
        _propertyGetters.Add(propertyName, expr);
    }

    return expr;
}

Here's the benchmark:

public class OrderBy
{

    private readonly List<Entry> _entries;

    public OrderBy()
    {
        _entries = new List<Entry>();
        for (int i = 0; i < 1_000_000; i++)
        {
            _entries.Add(new Entry($"Title {i}", i));
        }
    }

    [Benchmark(Baseline = true)]
    public List<Entry> SearchTitle()
    {
        return _entries.AsQueryable().OrderByDescending(p => p.Title).ToList();
    }

    [Benchmark]
    public List<Entry> SearchTitleDynamicallyWithoutCache()
    {
        var expr = DynamicExpressions<Entry>.GetPropertyGetter("Title");
        return _entries.AsQueryable().OrderByDescending(expr).ToList();
    }

    [Benchmark]
    public List<Entry> SearchTitleDynamicallyWithCache()
    {
        var expr = DynamicExpressions<Entry>.GetPropertyGetter("Title", useCache: true);
        return _entries.AsQueryable().OrderByDescending(expr).ToList();
    }

}

public class Entry
{

    public string Title { get; set; }
    public int Number { get; set; }

    public Entry(string title, int number)
    {
        Title = title;
        Number = number;
    }

}

And here are the results:
enter image description here

So my question is, why does creating an expression (which uses Reflection to get the property) faster than a direct access (p => p.Title)?


Solution

  • The problem is your GetPropertyGetter method generates a lambda that converts the result of the property into an object. When OrderBy sorts by object instead of by string, the comparison used is different. If you change the lambda to p => (object)p.Title you will discover it is faster as well. If you change the OrderByDescending to take a StringComparer.InvariantCulture, you will see a slight speed up over your generated lambdas.

    Of course, that also means your dynamic OrderBy most likely doesn't handle other languages properly.

    Unfortunately once you start to dynamically create code like the lambda for a LINQ method, you can't always just substitute object and expect the same results (e.g. an int field will be boxed, string won't use the same comparer, types with custom comparers may not work, ...). Basically I think of Expression building for dynamic type handling as like the GPL - it spreads out (and up) like a virus. If you replaced OrderByDescending(GetPropertyGetter) with dynamic OrderByPropertyNameDescending(string) and built the call to OrderBy as well, you would get what you expect.

    Consider:

    public static class DynanmicExt {
        public static IOrderedQueryable<TEntity> OrderByDescending<TEntity>(this IQueryable<TEntity> q, string propertyName) {
            var entityType = typeof(TEntity);
            var property = entityType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
            if (property == null)
                throw new Exception($"Property {propertyName} was not found in entity {entityType.Name}");
    
            var param = Expression.Parameter(typeof(TEntity));
            var prop = Expression.Property(param, propertyName);
            var expr = Expression.Lambda<Func<TEntity,string>>(prop, param);
    
            var OrderBymi = typeof(Queryable).GetGenericMethod("OrderByDescending", new[] { typeof(IQueryable<TEntity>), typeof(Expression<Func<TEntity, object>>) })
                                             .MakeGenericMethod(typeof(TEntity), prop.Member.GetMemberType());
            var obParam = Expression.Parameter(typeof(IQueryable<TEntity>));
            var obBody = Expression.Call(null, OrderBymi, obParam, expr);
            var obLambda = Expression.Lambda<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>>(obBody, obParam).Compile();
    
            return obLambda(q);
        }
    }
    

    Oh, almost forgot it needs these handy Reflection helpers:

    public static class MemberInfoExt {
        public static Type GetMemberType(this MemberInfo member) {
            switch (member) {
                case FieldInfo mfi:
                    return mfi.FieldType;
                case PropertyInfo mpi:
                    return mpi.PropertyType;
                case EventInfo mei:
                    return mei.EventHandlerType;
                default:
                    throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
            }
        }
    }
    
    public static class TypeExt {
        public static MethodInfo GetGenericMethod(this Type t, string methodName, params Type[] pt) =>
            t.GetMethods().Where(mi => mi.Name == methodName && mi.IsGenericMethod && mi.GetParameters().Select(mip => mip.ParameterType.IfGetGenericTypeDefinition()).SequenceEqual(pt.Select(p => p.IfGetGenericTypeDefinition()))).Single();
        public static Type IfGetGenericTypeDefinition(this Type aType) => aType.IsGenericType ? aType.GetGenericTypeDefinition() : aType;
    }
    

    Now you can use it with:

    public List<Entry> SearchTitle2() =>
        _entries.AsQueryable().OrderByDescending("Title").ToList();
    

    This is now just as slow as the lambda is to run.