【问题标题】:Why is creating and using an Expression faster than direct access?为什么创建和使用表达式比直接访问更快?
【发布时间】:2020-05-15 18:52:25
【问题描述】:

我目前正在实施一些动态过滤/排序,并认为做一个基准来了解情况是个好主意。

首先,这是创建充当“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;
}

这是基准:

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;
    }

}

结果如下:

所以我的问题是,为什么创建表达式(使用反射来获取属性)比直接访问 (p =&gt; p.Title) 更快?

【问题讨论】:

  • 从技术上讲,这些都是所有表达式。您必须查看为它们中的每一个生成的表达式树,以了解编译器自动为您生成的表达式树有什么不同。
  • 查看生成的IL或者粘贴到sharplab中。它将向您显示编译器为您的 lambda 创建的所有表达式。然后将其与您手动获得的内容进行比较

标签: c# .net-core benchmarking expression-trees


【解决方案1】:

问题是您的GetPropertyGetter 方法会生成一个lambda,该lambda 会将属性的结果转换为object。当OrderByobject 而不是string 排序时,使用的比较不同。如果您将 lambda 更改为 p =&gt; (object)p.Title,您会发现它也更快。如果您将 OrderByDescending 更改为 StringComparer.InvariantCulture,您将看到生成的 lambdas 略有加速。

当然,这也意味着您的动态 OrderBy 很可能无法正确处理其他语言。

不幸的是,一旦您开始为 LINQ 方法动态创建像 lambda 这样的代码,您不能总是只替换 object 并期望得到相同的结果(例如,int 字段将被装箱,string 不会' t 使用相同的比较器,带有自定义比较器的类型可能不起作用,...)。基本上我认为Expression 构建动态类型处理就像 GPL - 它像病毒一样传播(和向上)。如果您将OrderByDescending(GetPropertyGetter) 替换为动态OrderByPropertyNameDescending(string) 并建立对OrderBy 的调用,您将得到您所期望的。

考虑:

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);
    }
}

哦,差点忘了它需要这些方便的反射助手:

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;
}

现在您可以使用它:

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

这现在和 lambda 运行一样慢。

【讨论】:

  • 确实,使用(object)p.Title 确实使两者之间的比较更加接近。如果您当然有时间详细说明,我想知道您为什么将生成的表达式视为病毒。同时,我将此标记为答案。
  • @Haytam 我更新了我的答案以包含在运行时构建OrderBy 的代码。不确定如何更好地解释病毒 - 考虑一下,您决定在运行时为 OrderBy 创建 lambda,但您不能在静态类型编译时环境中使用它,因此您必须创建代码以调用 OrderBy 在运行。幸运的是,OrderBy 的返回类型是静态类型的,否则病毒会传播给它的调用者,等等。如果您是在运行时开始为 Select... 创建 lambda
猜你喜欢
  • 1970-01-01
  • 2022-08-06
  • 2015-10-03
  • 2020-11-08
  • 2015-01-21
  • 2018-12-09
  • 2015-12-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多