【问题标题】:Using Reflection to build EF Core query is faster than to using reflection使用反射构建 EF Core 查询比使用反射更快
【发布时间】:2021-09-24 21:52:51
【问题描述】:

我有一个 IQueryable 扩展方法,用于减少在 EF Core DbContext 模型中搜索多个字段所需的样板代码量:

public static IQueryable<TEntity> WherePropertyIsLikeIfStringIsNotEmpty<TEntity>(this IQueryable<TEntity> query,
    string searchValue, Expression<Func<TEntity, string>> propertySelectorExpression)
{
    if (string.IsNullOrEmpty(searchValue) || !(propertySelectorExpression.Body is MemberExpression memberExpression))
    {
        return query;
    }
    
    // get method info for EF.Functions.Like
    var likeMethod = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new []
    {
        typeof(DbFunctions),
        typeof(string),
        typeof(string)
    });
    var searchValueConstant = Expression.Constant($"%{searchValue}%");
    var dbFunctionsConstant = Expression.Constant(EF.Functions);
    var propertyInfo = typeof(TEntity).GetProperty(memberExpression.Member.Name);
    var parameterExpression = Expression.Parameter(typeof(TEntity));
    var propertyExpression = Expression.Property(parameterExpression, propertyInfo);
    
    
    var callLikeExpression = Expression.Call(likeMethod, dbFunctionsConstant, propertyExpression, searchValueConstant);
    var lambda = Expression.Lambda<Func<TEntity, bool>>(callLikeExpression, parameterExpression);
    return query.Where(lambda);
}

代码正在运行并产生预期的结果,但是我担心使用表达式和一些反射会降低性能。所以我使用内存数据库和 BenchmarkDotNet nuget 包设置了一个基准测试。这是基准:

using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.EntityFrameworkCore;

class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<Benchmark>();
        }
    }

    public class Benchmark
    {
        private Context _context;
        private string SearchValue1 = "BCD";
        private string SearchValue2 = "FG";
        private string SearchValue3 = "IJ";
        
        [GlobalSetup]
        public void Setup()
        {
            _context = new Context(new DbContextOptionsBuilder<Context>().UseInMemoryDatabase(Guid.NewGuid().ToString())
                .Options);

            _context.TestModels.Add(new TestModel(1, "ABCD", "EFGH", "HIJK"));
            _context.SaveChanges();
        }

        [GlobalCleanup]
        public void Cleanup()
        {
            _context.Dispose();
        }
        
        [Benchmark]
        public void FilterUsingExtension()
        {
            var _ = _context.TestModels
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value)
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue)
                .WherePropertyIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue)
                .ToList();
        }

        [Benchmark]
        public void FilterTraditionally()
        {
            var query = _context.TestModels.AsQueryable();
            if (!string.IsNullOrEmpty(SearchValue1))
            {
                query = query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"));
            }
            if (!string.IsNullOrEmpty(SearchValue2))
            {
                query = query.Where(x => EF.Functions.Like(x.OtherValue, $"%{SearchValue2}%"));
            }
            if (!string.IsNullOrEmpty(SearchValue3))
            {
                query = query.Where(x => EF.Functions.Like(x.ThirdValue, $"%{SearchValue3}%"));
            }
        
            var _ = query.ToList();
        }
    }

    public class TestModel
    {
        public int Id { get; }
        public string Value { get; }
        public string OtherValue { get; }
        public string ThirdValue { get; }

        public TestModel(int id, string value, string otherValue, string thirdValue)
        {
            Id = id;
            Value = value;
            OtherValue = otherValue;
            ThirdValue = thirdValue;
        }
    }
    
    public class Context : DbContext
    {

        public Context(DbContextOptions<Context> options)
            : base(options)
        {
            
        }
        
        // ReSharper disable once UnusedAutoPropertyAccessor.Global
        public DbSet<TestModel> TestModels { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<TestModel>().ToTable("test_class", "test");
            modelBuilder.Entity<TestModel>().Property(x => x.Id).HasColumnName("id").HasColumnType("int");
            modelBuilder.Entity<TestModel>().Property(x => x.Value).HasColumnName("value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().Property(x => x.OtherValue).HasColumnName("other_value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().Property(x => x.ThirdValue).HasColumnName("third_value").HasColumnType("varchar")
                .ValueGeneratedNever();
            modelBuilder.Entity<TestModel>().HasKey(x => x.Id);
        }
    }

就像我说的,我预计使用反射会导致性能下降。但基准测试表明,通过我的扩展方法构建的查询比直接在 Where 方法中编写表达式快 10 倍以上:

|               Method |        Mean |     Error |    StdDev |      Median |
|--------------------- |------------:|----------:|----------:|------------:|
| FilterUsingExtension |    73.73 us |  1.381 us |  3.310 us |    72.36 us |
|  FilterTraditionally | 1,036.60 us | 20.494 us | 22.779 us | 1,032.69 us |

谁能解释一下?

【问题讨论】:

  • 你生成的 SQL 是什么?另一个生成的是什么 SQL?
  • 内存数据库总是会更快地进行基准测试。反射与此无关。您是否在问为什么 In-Memory 数据库在基准测试中比传统的 SQL 支持的 DBContext 执行得更快?
  • 不知道,应该会慢一点。检查一下,也许 JIT 优化了整个测试体?

标签: c# .net-core reflection entity-framework-core expression


【解决方案1】:

简而言之,区别在于EF.Functions.Likepattern 参数的不同表达式,以及LINQ to Objects(EF Core InMemory 提供程序使用)处理IQueryable 表达式树的方式。

首先,使用 EF Core InMemory 提供程序对少量数据进行性能测试是无关紧要的,因为它基本上测量的是查询表达式树的构建,而在真实数据库的情况下,大部分时间是执行生成的 SQL 查询,返回并具体化结果数据集。

第二,关于

我担心使用表达式和一些反射会降低性能

这两种方法都使用Expression 类方法在运行时构建查询表达式树。唯一的区别是 C# 编译器在编译时为您生成该代码,因此没有反射调用。但是您的代码也可以轻松修改以避免反射,从而使生成完全等效。

更重要的区别是您的代码发出 ConstantExpression,而当前 C# 编译器无法从变量生成常量表达式,因此它总是发出闭包,而闭包又被 EF Core 查询翻译器绑定为查询参数.通常建议对 SQL 查询使用这种方法,因此您最好在方法中执行相同操作,或者可以选择这样做。

所以,简单回顾一下,您的方法绑定常量表达式,而编译器方法绑定闭包。但不仅如此。看这里

query.Where(x => EF.Functions.Like(x.Value, $"%{SearchValue1}%"))

SearchValue1 变量被转换为闭包,但由于$"%{SearchValue1}%"表达式 的一部分,因此此时不会对其进行评估,而是将其记录为MethodCallExpressionstring.Format

这两个在 LINQ to Objects 中产生了很大的性能差异,因为它通过首先将表达式编译为委托,然后运行它来执行查询表达式树。所以最后你的代码传递了常量值,编译器生成的查询代码调用string.Format。并且两者之间的编译/执行时间存在很大差异。在您的测试中乘以 3。


说了这么多,让我们看看实际情况。

首先,优化的扩展方法,一次性静态反射信息缓存和使用常量或变量的选项:

public static IQueryable<TEntity> WhereIsLikeIfStringIsNotEmpty<TEntity>(
    this IQueryable<TEntity> query,
    string searchValue,
    Expression<Func<TEntity, string>> selector,
    bool useVariable = false)
{
    if (string.IsNullOrEmpty(searchValue)) return query;
    var parameter = selector.Parameters[0];
    var pattern = Value($"%{searchValue}%", useVariable);
    var body = Expression.Call(LikeMethod, DbFunctionsArg, selector.Body, pattern);
    var predicate = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
    return query.Where(predicate);
}

static Expression Value(string value, bool variable)
{
    if (!variable) return Expression.Constant(value);
    return Expression.Property(
        Expression.Constant(new StringVar { Value = value }),
        StringVar.ValueProperty);
}

class StringVar
{
    public string Value { get; set; }
    public static PropertyInfo ValueProperty { get; } = typeof(StringVar).GetProperty(nameof(Value));
}

static Expression DbFunctionsArg { get; } = Expression.Constant(EF.Functions);

static MethodInfo LikeMethod { get; } = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new[]
{
    typeof(DbFunctions),
    typeof(string),
    typeof(string)
});

请注意,我从方法名称和MemberExpression 的要求中删除了Property,因为它不是必需的 - 该方法将适用于任何string 返回表达式。

其次,为其添加两个新的基准测试方法:


[Benchmark]
public void FilterUsingExtensionOptimizedUsingConstant()
{
    var _ = _context.TestModels
        .WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, false)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, false)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, false)
        .ToList();
}

[Benchmark]
public void FilterUsingExtensionOptimizedUsingVariable()
{
    var _ = _context.TestModels
        .WhereIsLikeIfStringIsNotEmpty(SearchValue1, testModel => testModel.Value, true)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue2, testModel => testModel.OtherValue, true)
        .WhereIsLikeIfStringIsNotEmpty(SearchValue3, testModel => testModel.ThirdValue, true)
        .ToList();
}

最后,为“传统方式”的优化版本添加基准,在表达式树中避免string.Format(但仍绑定变量):

[Benchmark]
public void FilterTraditionallyOptimized()
{
    var query = _context.TestModels.AsQueryable();
    if (!string.IsNullOrEmpty(SearchValue1))
    {
        var pattern = $"%{SearchValue1}%";
        query = query.Where(x => EF.Functions.Like(x.Value, pattern));
    }
    if (!string.IsNullOrEmpty(SearchValue2))
    {
        var pattern = $"%{SearchValue2}%";
        query = query.Where(x => EF.Functions.Like(x.OtherValue, pattern));
    }
    if (!string.IsNullOrEmpty(SearchValue3))
    {
        var pattern = $"%{SearchValue3}%";
        query = query.Where(x => EF.Functions.Like(x.ThirdValue, pattern));
    }

    var _ = query.ToList();
}

结果:

Method Mean Error StdDev
FilterUsingExtension 51.84 us 0.089 us 0.079 us
FilterUsingExtensionOptimizedUsingConstant 48.95 us 0.061 us 0.054 us
FilterUsingExtensionOptimizedUsingVariable 58.40 us 0.354 us 0.331 us
FilterTraditionally 625.40 us 1.269 us 1.187 us
FilterTraditionallyOptimized 60.09 us 0.491 us 0.435 us

如我们所见,使用常量的优化扩展方法最快,但非常接近您的原始方法(这意味着反射不是必需的)。

带变量的变体速度稍慢,但在实际数据库中使用时通常会更好。

优化后的“传统”方法比前两种慢一点,这有点令人惊讶,但差异可以忽略不计。

由于上述原因,最初的“传统”方法比以前的所有方法都要慢。但是对于真正的数据库,它在整个查询执行中将是可以忽略不计的一部分。

【讨论】:

  • 啊,错过了字符串插值。很好的收获。
  • 哦,这很有意义。我完全错过了字符串插值。非常感谢您的详细解答
  • 嗨,Ivan,您有时间可以帮我一个忙吗?我问你是因为你是 SO 最好的 EF 专家。我正在回答stackoverflow.com/questions/68471904/… 的问题,想知道您能否抽出时间来帮助我?我想知道是否可以将属性 ICollection Recruits 添加到成员。可能吗?提前致谢。 PS。我没有创建帖子,因为这不是我的问题。
  • 嗨@Serge,抱歉回复晚了,这几天我很忙。当然,我会看看它。
猜你喜欢
  • 2015-08-26
  • 2022-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多