为什么创建和使用表达式比直接访问更快?
Why is creating and using an Expression faster than direct access?
我目前正在实施一些动态 filtering/sorting,我认为做一个基准测试看看效果如何是个好主意。
首先,这里是创建表达式作为 "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 => p.Title
)更快?
问题是您的 GetPropertyGetter
方法生成了一个将 属性 的结果转换为 object
的 lambda。当 OrderBy
按 object
排序而不是按 string
排序时,使用的比较是不同的。如果将 lambda 更改为 p => (object)p.Title
,您会发现它也更快。如果您将 OrderByDescending
更改为 StringComparer.InvariantCulture
,您会发现生成的 lambda 表达式的速度略有提高。
当然,这也意味着您的动态 OrderBy
很可能无法正确处理其他语言。
不幸的是,一旦您开始为 LINQ 方法动态创建类似 lambda 的代码,您不能总是只替换 object
并期望得到相同的结果(例如 int
字段将被装箱, string
不会使用相同的比较器,具有自定义比较器的类型可能无法工作,...)。基本上,我认为 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 一样慢 运行。
我目前正在实施一些动态 filtering/sorting,我认为做一个基准测试看看效果如何是个好主意。
首先,这里是创建表达式作为 "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 => p.Title
)更快?
问题是您的 GetPropertyGetter
方法生成了一个将 属性 的结果转换为 object
的 lambda。当 OrderBy
按 object
排序而不是按 string
排序时,使用的比较是不同的。如果将 lambda 更改为 p => (object)p.Title
,您会发现它也更快。如果您将 OrderByDescending
更改为 StringComparer.InvariantCulture
,您会发现生成的 lambda 表达式的速度略有提高。
当然,这也意味着您的动态 OrderBy
很可能无法正确处理其他语言。
不幸的是,一旦您开始为 LINQ 方法动态创建类似 lambda 的代码,您不能总是只替换 object
并期望得到相同的结果(例如 int
字段将被装箱, string
不会使用相同的比较器,具有自定义比较器的类型可能无法工作,...)。基本上,我认为 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 一样慢 运行。